Skip to content

Unwrap Annotated type hints in type resolution and comparison - #7189

Open
masenf wants to merge 2 commits into
mainfrom
claude/sleepy-brown-b1b0b5
Open

masenf wants to merge 2 commits into
mainfrom
claude/sleepy-brown-b1b0b5

Conversation

@masenf

@masenf masenf commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Description

This PR fixes type resolution and comparison to properly handle Annotated type hints, which are commonly used in pydantic for discriminated unions and other metadata-carrying types.

Problem: When a var is typed with a pydantic discriminated union like Annotated[Cat | Dog, Field(discriminator="kind")], reading it from a state var (e.g., colors["red"] where colors: dict[str, Annotated[...]]) would fail with "Unsupported type ... for guess_type" because the type resolution logic didn't unwrap the Annotated wrapper to see the actual union type.

Solution:

  1. Added _annotated_origin() helper to safely extract the wrapped type from Annotated[X, ...] hints
  2. Updated resolve_type_alias() to unwrap Annotated before resolving aliases, handling cases like Annotated[SomeAlias, ...]
  3. Updated typehint_issubclass() to unwrap Annotated on both sides of the comparison so that Annotated[int, "meta"] correctly compares as int

The metadata (pydantic discriminators, validators, units, etc.) is never part of the type Reflex reasons about, so it's safely discarded during type inspection.

Changes

  • packages/reflex-base/src/reflex_base/utils/types.py:

    • Added _annotated_origin() helper function
    • Modified resolve_type_alias() to unwrap Annotated hints before alias resolution
    • Modified typehint_issubclass() to unwrap Annotated on both sides of comparison
  • tests/units/reflex_base/utils/test_types.py:

    • Added test_resolve_type_alias_unwraps_annotated() covering nested annotations and aliases
    • Added test_typehint_issubclass_unwraps_annotated() covering comparison with annotations
    • Added test_isinstance_unwraps_annotated() covering instance validation
  • tests/units/vars/test_object.py:

    • Added test_annotated_value_type_is_unwrapped() testing pydantic discriminated unions in dict values
    • Added test_annotated_var_type_is_unwrapped() testing Annotated on var's own type
  • packages/reflex-base/news/+annotated-type-hints.bugfix.md:

    • Added changelog fragment documenting the fix

Testing

All new tests pass and cover:

  • Unwrapping nested Annotated hints
  • Unwrapping Annotated around type aliases
  • Type comparison with Annotated on either side
  • Instance validation against Annotated types
  • Real-world pydantic discriminated union scenario (reading from dict var)

https://claude.ai/code/session_01HxGHX7e4xnf1ehT1H1h6Nz

Review in cubic

`guess_type` crashed with `Unsupported type typing.Annotated[...] for
guess_type.` on a var whose type is a pydantic discriminated union, which is
spelled `Annotated[A | B, Field(discriminator=...)]`. Reading one out of a
state var — `State.catalog.paint_colors[color_id]` — failed at compile time,
as did reaching any attribute on it.

`guess_type` normalizes its type through `resolve_type_alias` before
inspecting it, so unwrap `Annotated` there: the metadata is never part of the
type Reflex reasons about, and stripping it early also lets the var carry the
annotated type, so attribute access and validation resolve through the union
as usual. `_isinstance` and `typehint_issubclass` pick up the same treatment
through their existing calls.

`typehint_issubclass` also compared an `Annotated` hint structurally. Reflex's
`get_origin` reports `X` rather than `Annotated` for `Annotated[X, ...]`, so
the origin/args comparison read the hint as a bare `X` carrying the metadata
as a type argument, and `Annotated[A | B, ...]` was not a subclass of `A | B`
even though `A` was a subclass of `Annotated[A | B, ...]`. Unwrap both sides
up front so the comparison is symmetric.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxGHX7e4xnf1ehT1H1h6Nz
@masenf
masenf requested a review from a team as a code owner September 17, 2026 20:58
@masenf
masenf marked this pull request as draft September 17, 2026 20:59
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxGHX7e4xnf1ehT1H1h6Nz
@greptile-apps

greptile-apps Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with no actionable correctness, security, or repository-rule violations identified.

Summary

This PR updates Reflex’s type inspection utilities to treat Annotated[X, ...] as X, including through PEP 695 aliases, unions, type comparisons, instance checks, and Var type inference.

  • Adds a focused helper for extracting the annotated type.
  • Normalizes annotations during alias resolution and subclass comparison.
  • Adds regression coverage for nested annotations, aliases, containers, discriminated unions, and object Var access.
  • Adds the required user-facing bugfix news fragment.

Reviews (1) · Last reviewed commit: "Name the news fragment after PR 7189"

@codspeed

codspeed Bot commented Sep 17, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 53 untouched benchmarks
⏩ 9 skipped benchmarks1


Comparing claude/sleepy-brown-b1b0b5 (8c5a997) with main (00228b8)

Open in CodSpeed

Footnotes

  1. 9 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@masenf
masenf marked this pull request as ready for review September 17, 2026 23:05

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Re-trigger cubic

@FarhanAliRaza FarhanAliRaza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this PR in an example app that runs against this branch.

The app has a pydantic discriminated union, Annotated[A | B, Field(discriminator="kind")]. It uses the union as a dict value type, as a list item type in rx.foreach, and as a direct state var. It also has an Annotated[int, "meta"] var that feeds rx.text, an arithmetic expression, and the rx.progress value prop. An event handler takes an Annotated[int, "meta"] argument. A plain int var is the control.

On main, the page raises TypeError: Unsupported type typing.Annotated[...] for guess_type. On this branch, the page compiles and renders. I changed each var through its event handler in the browser. The DOM showed the correct values each time. The browser console and the backend log had no errors.

I also ran the unit tests for vars, reflex_base.utils and components. They pass.

One inline comment has a requested change. It is not blocking.

Comment on lines +507 to +509
if getattr(cls, "__metadata__", None) is None:
return None
return getattr(cls, "__origin__", None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two getattr calls run for each typehint_issubclass call and at each recursion level. For a hint without Annotated, both calls miss. The miss is slow on a types.GenericAlias, because the alias forwards the unknown attribute to its origin and that lookup fails too.

Measured, 50k calls of typehint_issubclass(list[int], list[int]), minimum of 5 repeats: 0.087 s on main, 0.116 s on this branch (+28%).

A type-identity check removes the cost. With this change the same call takes 0.088 s, and the tests of this PR pass:

_AnnotatedAlias = type(Annotated[int, ""])


def _annotated_origin(cls: Any) -> Any:
    return cls.__origin__ if type(cls) is _AnnotatedAlias else None

It uses only the public typing.Annotated. It is also exact: a user class that defines __metadata__ and __origin__ can no longer match, so the docstring paragraph about the two attributes can go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants