Skip to content

[conformance] Model Annotated with a distinct Type case#2572

Closed
fangyi-zhou wants to merge 4 commits intofacebook:mainfrom
fangyi-zhou:add-type-annotated-variant
Closed

[conformance] Model Annotated with a distinct Type case#2572
fangyi-zhou wants to merge 4 commits intofacebook:mainfrom
fangyi-zhou:add-type-annotated-variant

Conversation

@fangyi-zhou
Copy link
Copy Markdown
Contributor

Summary

Before this change, we are treating an annotated type Annotated[T, ...] as the base type T. This is fine in the majority of the places, but does not capture some subtleties according to the spec.

We have failing conformance test cases due to an Annotated type being accepted as a type or TypeAlias, as well as being callable, whereas the spec doesn't allow so.

https://typing.python.org/en/latest/spec/qualifiers.html#annotated

As with most special forms, Annotated is not assignable to type or type[T]

An attempt to call Annotated (whether parameterized or not) should be treated as a type error by type checkers

This diff adds a new Type case for Annotated, which contains the base type. The type behaves like the base type, except it cannot be passed to type or called.

Test Plan

Ran conformance testsuite, and observed that qualifiers_annotated.py is passing.

…8-88)

Introduce `Type::Annotated(Box<Type>)` — a thin wrapper produced when
`Annotated[T, meta]` is used as a value expression (not in an annotation).
This makes `Annotated[int, ""]` and TypeAliases of it non-callable and
not assignable to `type[T]`, matching the typing spec.

Key changes:
- `types.rs`: Add `Type::Annotated` variant with Visit/VisitMut impls
- `display.rs`: Display as `Annotated[T]`
- `specials.rs`: Produce `Type::Annotated(inner)` instead of `Type::Type(inner)`
- `solve.rs`: `untype_opt` unwraps Annotated in annotation context;
  `check_type_form` recognizes Annotated as a valid type form;
  `as_type_alias` preserves the Annotated wrapper for TypeAliases;
  `tvars_to_tparams_for_type_alias` recurses into Annotated
- `subset.rs`: Reject `Annotated(_) <: Type(_)` (not assignable to type[T])
- `attr.rs`: Add Annotated to the no-attribute catch-all arm
- conformance: Update expected results (6 → 0 failures in qualifiers_annotated.py)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@meta-cla meta-cla bot added the cla signed label Feb 26, 2026
@github-actions

This comment has been minimized.

fangyi-zhou and others added 2 commits February 27, 2026 00:31
`Type::Annotated` was not handled in `as_attribute_base1`, causing it to
fall into the no-op catch-all and produce an internal error whenever an
attribute was accessed on an `Annotated[...]` type form (e.g.
`Annotated[int, "meta"].__doc__`).

Fix by mapping `Type::Annotated` to `ClassInstance(generic_alias)`,
which matches the runtime representation: `Annotated[T, ...]` is an
instance of `typing._AnnotatedAlias`, a subclass of `typing._GenericAlias`.

This was caught by the mypy_primer test in the PR CI run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fangyi-zhou fangyi-zhou changed the title Model Annotated with a new Type case Model Annotated with a distinct Type case Feb 27, 2026
@github-actions
Copy link
Copy Markdown

Diff from mypy_primer, showing the effect of this PR on open source code:

Tanjun (https://github.com/FasterSpeeding/Tanjun)
+ ERROR tanjun/annotations.py:1601:12-43: Returned type `Annotated[_T]` is not assignable to declared return type `type[_T]` [bad-return]
+ ERROR tanjun/annotations.py:1855:16-42: Returned type `Annotated[str]` is not assignable to declared return type `type[str]` [bad-return]
+ ERROR tanjun/annotations.py:2268:16-64: Returned type `Annotated[PartialChannel]` is not assignable to declared return type `type[PartialChannel]` [bad-return]

pydantic (https://github.com/pydantic/pydantic)
+ ERROR pydantic/types.py:230:12-235:6: Returned type `Annotated[int]` is not assignable to declared return type `type[int]` [bad-return]
+ ERROR pydantic/types.py:491:12-497:6: Returned type `Annotated[float]` is not assignable to declared return type `type[float]` [bad-return]
+ ERROR pydantic/types.py:679:12-683:6: Returned type `Annotated[bytes]` is not assignable to declared return type `type[bytes]` [bad-return]
+ ERROR pydantic/types.py:817:12-828:6: Returned type `Annotated[str]` is not assignable to declared return type `type[str]` [bad-return]
+ ERROR pydantic/types.py:852:12-87: Returned type `Annotated[set[HashableItemType]]` is not assignable to declared return type `type[set[HashableItemType]]` [bad-return]
+ ERROR pydantic/types.py:868:12-93: Returned type `Annotated[frozenset[HashableItemType]]` is not assignable to declared return type `type[frozenset[HashableItemType]]` [bad-return]
+ ERROR pydantic/types.py:903:12-88: Returned type `Annotated[list[AnyItemType]]` is not assignable to declared return type `type[list[AnyItemType]]` [bad-return]
+ ERROR pydantic/types.py:1124:12-1131:6: Returned type `Annotated[Decimal]` is not assignable to declared return type `type[Decimal]` [bad-return]
+ ERROR pydantic/types.py:2257:12-2261:6: Returned type `Annotated[date]` is not assignable to declared return type `type[date]` [bad-return]
+ ERROR pydantic/v1/fields.py:586:45-49: Argument `Annotated[T]` is not assignable to parameter `class_or_tuple` with type `tuple[type[Any], ...] | type[Any] | None` in function `pydantic.v1.utils.lenient_issubclass` [bad-argument-type]
+ ERROR pydantic/v1/types.py:832:20-24: Returned type `Annotated[T]` is not assignable to declared return type `type[JsonWrapper]` [bad-return]

prefect (https://github.com/PrefectHQ/prefect)
- ERROR src/prefect/blocks/core.py:1046:16-51: Returned type `tuple[BlockDocument, str | None]` is not assignable to declared return type `tuple[BlockDocument, str]` [bad-return]
+ ERROR src/prefect/blocks/core.py:1046:16-51: Returned type `tuple[BlockDocument, Name | None]` is not assignable to declared return type `tuple[BlockDocument, str]` [bad-return]

@fangyi-zhou fangyi-zhou marked this pull request as ready for review February 27, 2026 00:55
@fangyi-zhou fangyi-zhou changed the title Model Annotated with a distinct Type case [conformance] Model Annotated with a distinct Type case Feb 27, 2026
@github-actions
Copy link
Copy Markdown

Primer Diff Classification

❌ 2 regression(s) | ➖ 1 neutral | 3 project(s) total

2 regression(s) across Tanjun, pydantic. error kinds: bad-return, bad-argument-type. caused by mk_type_form().

Project Verdict Changes Error Kinds Root Cause
Tanjun ❌ Regression +3 bad-return mk_type_form()
pydantic ❌ Regression +11 bad-argument-type, bad-return pyrefly/lib/alt/specials.rs
prefect ➖ Neutral +1, -1 bad-return
Detailed analysis

❌ Regression (2)

Tanjun (+3)

These are new errors where pyrefly now treats Annotated[T, ...] as incompatible with type[T] in value contexts. While this may be technically more accurate to runtime behavior (since Annotated[int, ...] creates an _AnnotatedAlias object, not int itself), this appears to be stricter than the established ecosystem standard. The typing spec treats Annotated[T, ...] as equivalent to T for type checking, and most code expects to use them interchangeably. The functions like _annotated() are clearly designed to return type aliases that should work as type objects. This represents pyrefly being more restrictive than the practical typing ecosystem expects.
Attribution: The change in pyrefly/lib/alt/specials.rs modified how Annotated is handled - instead of unwrapping to the inner type with mk_type_form(), it now creates a distinct Type::Annotated variant. The subset checking logic in pyrefly/lib/solver/subset.rs was updated to explicitly reject assignments from Type::Annotated(_) to Type::Type(_), causing these new errors.

pydantic (+11)

These are regressions. While the code has a theoretical type-level inconsistency (returning Annotated[T, ...] instead of type[T]), pyrefly is being stricter than the established ecosystem standard. Mypy and pyright don't flag these patterns because they work correctly in practice - the functions return type aliases that behave appropriately when used in annotations. The errors create noise without practical value since the code functions as intended.
Attribution: The change to introduce Type::Annotated in pyrefly/lib/alt/specials.rs and the subset checking logic in pyrefly/lib/solver/subset.rs that prevents Annotated[T, ...] from being assigned to type[T] caused these errors.

➖ Neutral (1)

prefect (+1, -1)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

Suggested Fix

Summary: The PR introduced a new Type::Annotated variant that prevents Annotated[T, ...] from being assignable to type[T], causing regressions in Tanjun and pydantic where this pattern is commonly used.

1. In is_subtype_of() in solver/subset.rs, add a special case for Type::Annotated: when checking if Annotated[T, ...] is assignable to type[U], unwrap the Annotated and check if T is assignable to type[U] instead. This restores the previous behavior where Annotated types were treated as equivalent to their inner type for assignment purposes.

Files: pyrefly/lib/solver/subset.rs
Confidence: high
Affected projects: Tanjun, pydantic
Fixes: bad-assignment, bad-argument-type
The regression analysis clearly shows that the subset checking logic now explicitly rejects assignments from Type::Annotated() to Type::Type(). Adding a special case to unwrap Annotated types when checking assignability to type[T] would restore the ecosystem-standard behavior. Expected outcome: eliminates 14 bad-assignment and bad-argument-type errors across 2 projects

2. In check_callable() in solver/subset.rs or the relevant callable checking function, add a guard: when checking if Type::Annotated(inner) is callable, delegate to checking if the inner type is callable. This prevents not-callable errors when Annotated types wrap callable types.

Files: pyrefly/lib/solver/subset.rs
Confidence: medium
Affected projects: Tanjun, pydantic
Fixes: not-callable
The conformance test shows new not-callable errors for Annotated types. Since Annotated[T, ...] should behave like T for most purposes, callable checking should unwrap the annotation. Expected outcome: eliminates 2 not-callable errors in conformance tests


Classification by primer-classifier (1 heuristic, 2 LLM) · Was this helpful? React with 👍 or 👎

@migeed-z
Copy link
Copy Markdown
Contributor

I'm working on a tool to classify those errors easily. Those look like expected errors to me. I think the LLM classified them as regressions, because it doesn't have the PR title and explanation. It thinks the regressions were not intentional and its feedback is essentially undoing the PR. I will fix that.

@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync bot commented Feb 27, 2026

@migeed-z has imported this pull request. If you are a Meta employee, you can view this in D94607593.

@migeed-z
Copy link
Copy Markdown
Contributor

Hmm.. so the diff ended up introducing 100+ errors on IG. cc @rchen152 for opinions on this.

@rchen152
Copy link
Copy Markdown
Contributor

Hmm.. so the diff ended up introducing 100+ errors on IG. cc @rchen152 for opinions on this.

I only looked at a few errors, but the ones I dug into were caused by pyrefly no longer treating things like Annotated[int, "metadata"] as callable. As per https://discuss.python.org/t/is-annotated-compatible-with-type-t/43898, this (pyrefly's new behavior as of this PR) is correct and spec-compliant, and IG was essentially relying on a bug in pyrefly.

@fangyi-zhou
Copy link
Copy Markdown
Contributor Author

fangyi-zhou commented Feb 27, 2026

The errors are true positives given a strict reading of the spec, in practice people have been using that in the wild quite often.

See discussion here

https://discuss.python.org/t/is-annotated-compatible-with-type-t/43898

Copy link
Copy Markdown
Contributor

@grievejia grievejia left a comment

Choose a reason for hiding this comment

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

Review automatically exported from Phabricator review in Meta.

@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync bot commented Mar 2, 2026

@migeed-z merged this pull request in 14be156.

meta-codesync bot pushed a commit that referenced this pull request Mar 4, 2026
#2634)

Summary:
Fixes #2633, a regression introduced by #2572, where we model `Annotated` in a separate constructor.

There are two issues here:
1. Type aliases for `Annotated` types are not resolved (thus being an `UntypedAlias`): this is because `Annotated` is now its own type, which is not always a `Type` for triggering the type alias resolution. The fix is to handle `Annotated` type in the resolution processing, and removing the `Annotated` wrapper.
2. `Annotated` types are not handled in `Literal` check: this is a straightforward fix to check the inner type when encountering an `Annotated` type.

Pull Request resolved: #2634

Test Plan: Added a regression test.

Reviewed By: stroxler, yangdanny97

Differential Revision: D95167871

Pulled By: rchen152

fbshipit-source-id: a61e259498d616dfb2ed4bb1ca4176eff8884e25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants