From 9dea6b37cf5e2a51e673f899e5e2803a828738c1 Mon Sep 17 00:00:00 2001 From: Till Varoquaux Date: Sun, 5 Jul 2026 22:58:23 -0400 Subject: [PATCH 1/2] PEP 835: Address Discourse feedback and align better with typing spec Key changes: - Structure & Formatting: Reordered all sections to strictly comply with the PEP 1 template. Added a mandatory "Security Implications" section. - Terminology & Spec Alignment: Consolidated terminology to provide clear definitions for the concepts used throughout the PEP (Type Expression, Type Metadata, Non-Typing Annotation, Symbol Decorator) and align better with the typing spec. - Forward References: Clarified that `Format.TYPE` supports `None @Metadata` structurally, eliminating the need to modify `NoneType.__matmul__`. Consequently, removed the "Supporting None" open issue. - Rejected Ideas: Expanded the section to include "Alternative Syntaxes" (e.g. `<@`, soft keywords, brackets), noting their rejection due to lack of consensus. Moved the `__rmatmul__` rejection out of the Specification section. - Future Work: Added extensive examples of how the syntax might map to future Symbol Decorators, and added a JSR 308-inspired proposal for PEP 746 target-based constraints using intersection types (`&`). --- peps/pep-0835.rst | 869 +++++++++++++++++++++++++++++++--------------- 1 file changed, 580 insertions(+), 289 deletions(-) diff --git a/peps/pep-0835.rst b/peps/pep-0835.rst index 68cfa2b299d..64574ae08fc 100644 --- a/peps/pep-0835.rst +++ b/peps/pep-0835.rst @@ -14,187 +14,221 @@ Post-History: `19-Apr-2026 `__ using the +``@`` operator. This change improves the ergonomics of type constraints and +reduces signature verbosity, benefiting type-directed libraries like +Pydantic, FastAPI, Typer, and SQLModel. Motivation ========== -Since its introduction in :pep:`593`, ``Annotated`` has become the standard -mechanism for attaching context-specific metadata to types. It is widely -embraced by libraries such as Pydantic, FastAPI, and SQLModel. -Historically, these libraries embedded metadata within default values, a pattern -that was concise but problematic for type checkers and runtime defaults: +Metadata as a Semantic Operation +-------------------------------- + +Historically, Python has treated type metadata as an afterthought. +``Annotated[T, M]`` models metadata as a generic container, similar to how +``Union[X, Y]`` once modeled unions. But metadata is not a container; it is a +modifier applied to a type. + +Just as :pep:`604` evolved unions from the ``Union`` container to the ``|`` +operator, the ``@`` syntax evolves metadata into a native language operator. +Elevating metadata to first-class syntax recognizes that modern libraries +increasingly use constraints on types (e.g., bounds, shapes, patterns). + +Previously, applying these constraints forced a frustrating compromise between +type safety and readability. Before :pep:`593` (``Annotated``), frameworks like +Pydantic and FastAPI embedded metadata directly into default values. This was +concise but broke static analysis: .. code-block:: :class: bad - # The older, now discouraged pattern class User(BaseModel): id: int = Field(gt=0) name: str = Field(min_length=3) -The transition to ``Annotated`` cleanly separated types from metadata but -introduced visual noise and cognitive overhead. Library authors often surface -"special" types, like ``PositiveInt`` or ``EmailStr``, to hide this verbosity. -These aliases are discoverable but inherently limited. Users needing unique -constraint combinations must fall back to the full ``Annotated`` syntax:: +Moving to ``Annotated`` fixed the typing semantics but introduced significant +complexity. To hide the visual noise, libraries introduced alias types (e.g., +``PositiveInt``). However, the moment users step outside pre-packaged +constraints, they are forced back into the complex syntax:: from typing import Annotated from pydantic import BaseModel, Field, PositiveInt class User(BaseModel): - # Concise but limited - age: PositiveInt + age: PositiveInt # Concise but limited + id: Annotated[int, Field(gt=0, le=1000)] # Verbose fallback - # Verbose fallback required for specific constraints - id: Annotated[int, Field(gt=0, le=1000)] - name: Annotated[str, Field(min_length=3, max_length=50)] = "Anonymous" +The ``@`` shorthand resolves the ergonomic tradeoff, restoring the conciseness +of early frameworks while preserving the strict semantics of ``Annotated``: -This creates a jarring experience. ``Annotated`` is core to the ecosystem but -remains "hidden" and difficult to use directly. The proposed shorthand bridges -this ergonomic gap. It restores the conciseness of earlier patterns while -adhering to the modern ``Annotated`` standard. By reducing overhead, this -proposal encourages developers to leverage the full power of type metadata: - -.. code-block:: python +.. code-block:: :class: good from pydantic import BaseModel, Field from fastapi import Query class User(BaseModel): - id: int @ Field(gt=0, le=1000) - name: str @ Field(min_length=3, max_length=50) = "Anonymous" - age: int @ Field(gt=0) - email: str @ Field(pattern=r".*@.*") + id: int @Field(gt=0, le=1000) + name: str @Field(min_length=3, max_length=50) = "Anonymous" + age: int @Field(gt=0) + email: str @Field(pattern=r".*@.*") - async def read_items(q: (str | None) @ Query(max_length=50) = None): + async def read_items(q: (str | None) @Query(max_length=50) = None): ... -Rationale -========= - -Developer Ergonomics and Ecosystem Alignment ---------------------------------------------- - -Pydantic and FastAPI now recommend ``Annotated`` over embedding metadata in -default values. However, the resulting code is significantly more verbose. -This proposal restores the earlier pattern's conciseness while adhering to the -modern ``Annotated`` standard. - -Sebastián Ramírez (author of FastAPI, Typer, and SQLModel) noted that a shorter -syntax without extra imports would benefit users. By making the "correct" way -the most ergonomic, we reduce the incentive for discouraged patterns. - -This ergonomic barrier was notably evident in the withdrawal of :pep:`727` -(Documentation Metadata). The extreme verbosity of the syntax in function -signatures was a primary factor in its community pushback. A native shorthand -makes such metadata-heavy standards significantly more viable. - -Conceptual Consistency and Precedent -------------------------------------- - -The ``@`` operator signifies "decoration" or "attachment of metadata" for -functions and classes. Extending this to type expressions leverages that -mental model: just as a decorator attaches behavior to a function, the ``@`` -operator attaches metadata to a type. - -This follows the precedent set by :pep:`604` (``|`` for ``Union``) and -:pep:`585` (generics in built-ins). These PEPs moved common typing constructs -into native operators, making the type system feel like a first-class part of -the language. - -This syntax also draws inspiration from other languages with strong metadata -ecosystems, notably Java. In Java (formalized in `JSR 308 `__) -and other JVM languages, the ``@`` symbol is standard for type annotations: - -.. code-block:: java - - public class Person { - @Column(length = 32) - private String name; - } - -While the exact syntax differs (Python's ``@`` operates inline on the type -expression rather than decorating the declaration), the visual association -between the ``@`` symbol and type-level metadata will be familiar to many -developers. - -Implementation and Performance -------------------------------- - -Making the syntax built-in eases runtime metadata use by removing ``typing`` -module import overhead. This aligns with the trend toward accessible runtime -type introspection. - -The proposed syntax is straightforward to implement. Prototypes for Mypy, -Pyright, and Ruff are compact. Since ``@`` is already a valid expression -operator, these tools do not require parser changes. They handle the new syntax -during semantic analysis. Ruff has already prototyped a ``pyupgrade`` rule -for automated conversion. This enables large codebases to -migrate to the new syntax with minimal manual effort. +Prior Art and Historical Context +-------------------------------- + +The verbosity of ``Annotated`` is not just an aesthetic annoyance; it hinders +adoption. During the 2023 review of :pep:`727`, reviewers warned that forcing +developers to use ``Annotated`` for everyday documentation would introduce +excessive visual noise [1]_. This pushback led to the PEP's withdrawal and +sparked the initial discussions around using ``@`` as a native alternative. + +Today, this friction remains evident across the entire ecosystem. The most +prominent type-directed frameworks in Python (FastAPI, Pydantic, SQLAlchemy, +cattrs, msgspec, Typer, and beartype) all heavily rely on ``Annotated`` [2]_ +[3]_ [4]_ [5]_ [6]_ [7]_ [8]_. Yet, they frequently receive user complaints +about signature bloat [9]_ [10]_ [11]_. In some domains, library authors refuse +to use it entirely: PyTorch [12]_, TorchTyping, and Beartype [13]_ rejected +``Annotated`` for tensor shape typing, deciding instead to wait for a native +language solution. + +When the community debated alternatives in late 2023 [14]_ and 2025 [15]_, +discussion trended toward the ``@`` operator. It visually aligns with existing +Python `decorators `__. +Adopting ``@`` for metadata follows the precedent of repurposing runtime +operators for static typing, as seen with ``[]`` for generics (:pep:`585`) and +``|`` for unions (:pep:`604`) [16]_. + +Precedent in Other Languages +---------------------------- -CPython prototype testing confirms that libraries like ``typer`` and -``pydantic`` work out of the box. +The ``@`` metadata syntax mirrors features in other statically typed languages: + +- **C++:** Uses ``[[attribute]]`` for both symbols and types (e.g., + ``[[nodiscard]] int f();``). +- **C#:** Uses ``[Attribute]`` for reflection-based metadata (e.g., + ``[Required] public string Name;``). +- **Java / Kotlin:** Uses ``@Annotation``. Java's `JSR 308 + `__ introduced ``TYPE_USE`` annotations + to decorate types (e.g., ``List<@NonNull String>``). Kotlin uses ``val x: + @NotNull String``. +- **OCaml:** Uses ``[@attribute]`` postfix syntax to attach metadata to the + preceding AST node (e.g., ``type t = int [@default 0]``). + +Terminology +=========== + +To clarify discussion around annotations and metadata, the following terms +apply: + +- **Type Expression**: e.g., ``x: ``. Expressions evaluating to valid + static types, as specified in the `Typing Specification + `__ + (:pep:`484`). +- **Type Metadata**: e.g., ``int @``. Data attached to a Type Expression + via ``Annotated`` (:pep:`593`, :pep:`746`). +- **Non-Typing Annotation**: e.g., `@no_type_check + `__ + wrapping ``x: ``. + Standard behavior where annotations are evaluated as arbitrary runtime values + rather than strict type hints. +- **Symbol Decorator**: Applying metadata directly to a variable or field + declaration, rather than to its underlying type. While Python does not + currently have a native syntax for variable decorators, this conceptual + distinction is well-established in languages like Java: + + .. code-block:: java + + class Application { + // Field Decorator (Symbol Decorator) + @Inject + private Service s; + + // Type Decorator (Type Metadata) + private @NonNull String name; + } + +- **Symbol Metadata** (or **Field Metadata**): Data attached to a symbol via a + Symbol Decorator. This contrasts with an active runtime descriptor. It is + fundamentally distinct from Type Metadata, as it applies to the specific + instance of the variable rather than the type itself. Specification ============= -The proposed syntax uses the ``@`` (matrix multiplication) operator to attach -metadata to a type:: +The proposed syntax uses the ``@`` (``__matmul__``) operator to attach metadata +to a type:: # Current syntax x: Annotated[int, Range(0, 10)] # Proposed shorthand - x: int @ Range(0, 10) + x: int @Range(0, 10) Operator Precedence ------------------- -The ``@`` operator has higher precedence than the ``|`` operator (bitwise OR, -used for Unions in :pep:`604`). Parentheses are required when attaching -metadata to a Union type: +The ``@`` operator binds tighter than the ``|`` union operator (:pep:`604`). +This aligns with standard Python expression precedence, ensuring predictable +parsing for Type Metadata. + +Because ``@`` binds tightly, developers can attach distinct constraints directly +to specific types within a union. For example, a US zip code might be a 5-digit +integer *or* a 5-character string:: + + zip_code: int @Ge(10000) @Le(99999) | str @Len(5) -- ``int | str @ Metadata`` is equivalent to ``int | Annotated[str, Metadata]`` -- ``(int | str) @ Metadata`` is equivalent to ``Annotated[int | str, Metadata]`` +If you intend to attach metadata to the entire union, you must use parentheses:: -This matches the standard precedence of ``@`` and ``|`` in Python expressions. -The most common union pattern, ``Optional``, works naturally: + # Attaches only to 'str' + int | str @Metadata # equivalent to: int | Annotated[str, Metadata] -- ``int @ Field(gt=0) | None`` is equivalent to - ``Annotated[int, Field(gt=0)] | None`` + # Attaches to the entire union + (int | str) @Metadata # equivalent to: Annotated[int | str, Metadata] -Flattening and Associativity +The "Parentheses Friction" +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some developers have noted that writing ``(str | None) @Field(...)`` feels +cumbersome for optional fields. + +This awkwardness highlights a missing language feature. Without native Symbol +Decorators, frameworks must co-opt ``Annotated`` to configure fields. When a +developer writes ``address: (str | None) @Field(...)``, they are actually +attempting to configure the ``address`` field, not the ``str | None`` +type. Until native field decorators exist, parentheses remain the structurally +accurate way to express this in the type system. + +Flattening Multiple Metadata ---------------------------- -The ``@`` operator is left-associative. When multiple metadata items are -chained, the resulting ``Annotated`` object is flattened. +When multiple metadata items are chained, the resulting ``Annotated`` object is +flattened. -Specifically, ``T @ m1 @ m2`` is strictly equivalent to -``Annotated[T, m1, m2]``. It must not resolve to a nested structure such as -``Annotated[Annotated[T, m1], m2]``. This mirrors the existing runtime -behavior of ``typing.Annotated``. +Specifically, ``T @m1 @m2`` evaluates to ``Annotated[T, m1, m2]``. It must not +resolve to a nested structure such as ``Annotated[Annotated[T, m1], m2]``. This +mirrors the existing runtime behavior of ``typing.Annotated``. This flattening also applies when the left-hand operand is an existing ``Annotated`` type, regardless of how it was constructed:: - Annotated[int, m1] @ m2 # AnnotatedType(int, m1, m2) — flattened + Annotated[int, m1] @m2 # AnnotatedType(int, m1, m2) (flattened) Runtime Behavior ---------------- The ``@`` operator produces a ``types.AnnotatedType`` instance, a new built-in type implemented in C. The existing ``typing.Annotated`` is unified with this -type: ``typing.Annotated[X, Y]`` returns the same ``types.AnnotatedType`` -object as ``X @ Y``: +type: ``typing.Annotated[X, Y]`` returns the same ``types.AnnotatedType`` object +as ``X @Y``: .. code-block:: pycon - >>> type(int @ Field()) is type(Annotated[int, Field()]) + >>> type(int @Field()) is type(Annotated[int, Field()]) True >>> typing.Annotated is types.AnnotatedType True @@ -205,215 +239,228 @@ An ``AnnotatedType`` object exposes the following attributes: - ``__metadata__``: A tuple of metadata items. - ``__args__``: The tuple ``(origin, *metadata)``, for compatibility with ``typing.get_args()``. -- ``__parameters__``: Lazily computed type variables contained in the type. +- ``__parameters__``: A tuple of unique free type parameters of the type. The ``repr()`` of an ``AnnotatedType`` uses the shorthand syntax: .. code-block:: pycon - >>> int @ Field(gt=0) - int @ Field(gt=0) + >>> int @Field(gt=0) + int @Field(gt=0) ``AnnotatedType`` objects support pickling via ``copyreg``, reconstructing through ``AnnotatedType[origin, *metadata]``. -``None`` on the left-hand side is accepted and uses ``None`` as the -origin: +Handling of ``None`` +-------------------- + +Implementing ``__matmul__`` on ``NoneType`` is explicitly avoided to prevent +masking runtime bugs in non-typing contexts. -.. code-block:: pycon +For example, a developer might forget to validate ``None`` before executing +matrix multiplication on an array: + +.. code-block:: + :class: bad + + def matmul_arrays(a: np.array | None, b: np.array): + return a @ b # oops, forgot to check for None + +If ``__matmul__`` were implemented on ``NoneType``, this would return an +``AnnotatedType`` object instead of raising a ``TypeError``, silently masking +the bug. - >>> None @ Field() - None @ Field() +Because ``Format.TYPE`` is introduced to ``annotationlib``, this limitation is +invisible in valid typing contexts. ``Format.TYPE`` evaluates +structurally and correctly parses ``None @Metadata`` into an ``AnnotatedType`` +without relying on ``NoneType.__matmul__``. Outside of Type Expressions +(e.g., in raw runtime execution), users must use ``Annotated[None, Metadata]``. Supported Left-Hand Operands ----------------------------- The ``@`` operator is implemented by adding ``nb_matrix_multiply`` to the -metatype (``type``) and to several typing-related types. The operator is -supported for any left-hand operand that currently supports the ``|`` -operator for making a union. +metatype (``type``) and to exactly the same typing constructs that currently +support the ``|`` operator for unions (``types.GenericAlias``, +``types.UnionType``, ``types.AnnotatedType``, ``typing.TypeVar``, +``typing.ParamSpec``, ``typing.TypeVarTuple``, ``typing.TypeAliasType``, +``typing.ForwardRef``, and ``sentinel`` objects). For all other left-hand operands, the operator returns ``NotImplemented``, allowing normal ``__matmul__`` dispatch to proceed. -Parsing and Grammar -=================== - -This proposal requires no changes to the Python grammar. Because ``@`` is -already a valid operator, it is natively parsed as a binary operation. The -shorthand is resolved during semantic analysis, entirely bypassing the need -to patch grammar files or update the parser. - -How to Teach This -================= - -In Python, the ``@`` symbol already has an established association with -metadata through decorators. The annotation shorthand extends this -intuition to the type system: ``int @ Field(gt=0)`` reads as "``int``, -decorated with ``Field(gt=0)``." +This means ``int @Field()`` produces an ``AnnotatedType``, while ``42 +@something`` is unaffected. Crucially, ``ndarray @Field()`` (using the **class** +as a type annotation) also produces an ``AnnotatedType``, even though ndarray +*instances* define ``__matmul__`` for matrix multiplication. One consequence of +this design is that applying the ``@`` operator to a class object evaluates as +Type Metadata, while applying it to an instance performs arithmetic. -For beginners, the key rule is simple: **in a type annotation, ``@`` means -"with this metadata."** The full ``Annotated[int, Field(gt=0)]`` syntax -remains available and is entirely equivalent for those who find it clearer. - -For experienced developers, the precedence rules follow standard Python -operator precedence (``@`` binds tighter than ``|``), and chaining -``T @ m1 @ m2`` flattens exactly as nested ``Annotated`` does. +The only case where the shorthand does not apply is when a class has a +**metaclass** that defines ``__matmul__``. In that case, the metaclass's +operator takes priority via standard Python MRO dispatch. -Documentation and teaching materials should introduce the shorthand alongside -``Annotated``, not as a replacement. The longhand form is still preferred in -contexts where multiple metadata items are passed as a group and chaining -would be unwieldy. -Backwards Compatibility -======================= Forward References and Deferred Evaluation ------------------------------------------- -Under :pep:`749`, annotations are lazily evaluated. The ``annotationlib`` -module provides several formats for retrieving annotations: - -- ``Format.VALUE``: Fully evaluates the annotation. Raises ``NameError`` - if any name is unresolvable. -- ``Format.FORWARDREF``: Wraps unresolvable names in ``ForwardRef`` objects. - However, compound expressions using operators (``@``, ``|``) produce an - opaque ``ForwardRef`` string containing the entire expression. -- ``Format.STRING``: Returns the raw source text with no evaluation. - -For the ``@`` operator, ``Format.FORWARDREF`` is insufficient. Consider:: +Under :pep:`749`'s lazy evaluation, existing ``annotationlib`` formats are +insufficient for ``@``. Specifically, ``Format.FORWARDREF`` fails structurally +on unresolvable names:: class Model: - ref: "NotYetDefined" @ Field(gt=0) + ref: NotYetDefined @Field(gt=0) -Under ``Format.FORWARDREF``, this produces -``ForwardRef('"NotYetDefined" @ Field(gt=0)')``. The metadata ``Field(gt=0)`` -is trapped inside the unresolved string and cannot be inspected until the -forward reference is resolved. This is a blocking issue for libraries like -Pydantic and FastAPI, which inspect metadata at class-definition time. +``Format.FORWARDREF`` produces an opaque ``ForwardRef('"NotYetDefined" +@Field(gt=0)')``. The metadata is trapped inside the unresolved string, blocking +libraries like Pydantic from inspecting it at class-definition time. -This proposal introduces a new format, ``Format.FORWARDREF_STRUCTURAL``. -This format assumes typing semantics and evaluates compound type expressions -**structurally**. It always returns an ``AnnotatedType`` for ``@``, a union -for ``|``, and a ``GenericAlias`` for subscripting. When a name cannot be -resolved, only that name is wrapped in ``ForwardRef``; the surrounding -operators are still evaluated. The example above produces:: +``Format.TYPE`` assumes typing semantics and evaluates Type Expressions +structurally. Unresolvable names are wrapped in a ``ForwardRef`` independently, +leaving operators intact:: AnnotatedType(ForwardRef('NotYetDefined'), Field(gt=0)) -The metadata is immediately accessible. This format also resolves the -pre-existing issue with ``|`` unions, where ``"Foo" | int`` under -``Format.FORWARDREF`` produces ``ForwardRef('Foo | int')`` instead of the -structural ``ForwardRef('Foo') | int``. +The metadata remains immediately accessible. This also resolves the pre-existing +issue where ``"Foo" | int`` incorrectly produced ``ForwardRef('Foo | int')`` +under ``Format.FORWARDREF``. -Interaction with PEP 563 -^^^^^^^^^^^^^^^^^^^^^^^^^ +Importantly, because ``Format.TYPE`` can evaluate structurally when runtime +dunder methods are missing (falling back to ``types.UnionType`` and +``types.AnnotatedType`` for the ``|`` and ``@`` operators), it parses ``None +@Metadata`` without requiring ``NoneType.__matmul__``. -Under :pep:`563` (``from __future__ import annotations``), all annotations -are stored as source-code strings and evaluated via ``eval()`` on access. The -``@`` shorthand works correctly in this context: ``eval("int @ Field(gt=0)")`` -triggers the metatype's ``nb_matrix_multiply`` and produces an -``AnnotatedType``. +Parsing and Grammar +------------------- -However, ``FORWARDREF_STRUCTURAL`` reconstruction from PEP 563 strings is -coarser than from :pep:`749` thunks. When a name is unresolvable, the -``ForwardRef`` may wrap a call expression (e.g., ``ForwardRef('Field(gt=0)')``) -rather than just a name. :pep:`749` provides a strictly better experience and -is the recommended path forward. +No changes to the Python grammar are required. Because ``@`` is already a valid +operator, it is natively parsed as a binary operation. The shorthand is resolved +during semantic analysis, entirely bypassing the need to patch grammar files or +update the parser. -Operator Overloading --------------------- +Rationale +========= -The ``@`` operator is currently used for matrix multiplication -(``__matmul__``). The shorthand is implemented by adding -``nb_matrix_multiply`` to the metatype (``type``), so it applies when a -**type object** (class) appears on the left-hand side — not when an instance -does. +Baking ``Annotated`` into native syntax removes ``typing`` import overhead, +directly aiding runtime type introspection. -This means ``int @ Field()`` produces an ``AnnotatedType``, while -``42 @ something`` is unaffected and follows normal ``__matmul__`` dispatch. -Crucially, ``ndarray @ Field()`` (using the **class** as a type annotation) -also produces an ``AnnotatedType``, even though ndarray *instances* define -``__matmul__`` for matrix multiplication. This is the desired behavior: applying the -``@`` operator to a class object evaluates as type metadata; applying it to -an instance performs arithmetic. +Because ``@`` is already a valid expression operator, type checkers (Mypy, +Pyright) require no parser changes, handling the syntax entirely during semantic +analysis. We have prototyped an automated conversion rule in Ruff, enabling +automated codebase migrations, and CPython prototype testing confirms that +libraries like ``typer`` and ``pydantic`` work natively out of the box. -The only case where the shorthand does not apply is when a class has a -**metaclass** that defines ``__matmul__``. In that case, the metaclass's -operator takes priority via standard Python MRO dispatch. This is an obscure -edge case unlikely to arise in practice. +Why the @ Operator? +------------------- + +Selecting the correct operator for metadata involves balancing three grammatical +considerations: + +1. **Precedence & Spec Restrictions:** The typing specification currently only + allows the ``|`` operator in Type Expressions. Any new operator must bind + tighter than ``|`` (Union) and ``&`` (proposed Intersection) so that + expressions like ``int @Field() | str`` parse correctly without parentheses. + This eliminates operators like ``|``, ``^``, and ``&``. +2. **Backwards Compatibility & Consistency:** Using an existing operator is + preferable to introducing a new keyword (e.g., ``annotated``). It minimizes + parser complexity and mitigates the risk of breaking existing code. It also + aligns with the Python typing specification's precedent of preferring + operators (e.g., ``|``) over keywords within type expressions. +3. **Semantic Clarity:** The chosen syntax should avoid colliding with + established mathematical intuitions for primitive types. + +This leaves the set of overridable binary operators that bind tighter than +``&``: ``**``, ``*``, ``@``, ``/``, ``//``, ``%``, ``+``, ``-``, ``>>``, and +``<<``. + +Standard arithmetic operators like ``+``, ``-``, ``/``, ``//``, ``*``, ``**``, +and ``%`` are misleading in this context. Reading ``int + x`` or ``float / +Field()`` strongly implies mathematical evaluation, not metadata decoration. + +This leaves ``<<``, ``>>``, and ``@``. Of these, ``@`` is the only operator that +possesses an existing association with metadata in Python (via function and +class decorators). Bitwise shift operators (``<<``, ``>>``) lack this +association. + +While ``@`` is used for matrix multiplication in numeric libraries (like NumPy), +it is far less associated with core scalar arithmetic than operators like ``+`` +or ``/``. + +Language Complexity +------------------- + +Adding new syntax always introduces some language complexity. However, while +syntactic complexity increases, the *cognitive* load actually decreases. + +Teaching a beginner ``int @Field(gt=0)`` leverages their existing intuition of +Python decorators. It visually separates the base type from its metadata. +Teaching ``typing.Annotated`` requires referencing obscure parts of the typing +module. By moving metadata from a standard library class to a native grammatical +construct, we reduce the overall cognitive load required to read and write +modern typed Python. + +Backwards Compatibility +======================= typing.Annotated Migration --------------------------- -This proposal replaces the pure-Python ``typing._AnnotatedAlias`` class with -a native C implementation (``types.AnnotatedType``). ``typing.Annotated`` -becomes a reference to this C type rather than a special form with a custom -metaclass. +The pure-Python ``typing._AnnotatedAlias`` class is replaced with a native C +implementation (``types.AnnotatedType``). ``typing.Annotated`` becomes a +reference to this C type rather than a special form with a custom metaclass. The private ``typing._AnnotatedAlias`` class is retained as a deprecated -compatibility shim. Code using ``isinstance(x, typing._AnnotatedAlias)`` -will continue to work but emit a ``DeprecationWarning``. The shim is -scheduled for removal in Python 3.18. +compatibility shim. Code using ``isinstance(x, typing._AnnotatedAlias)`` will +continue to work but emit a ``DeprecationWarning``. The shim is scheduled for +removal in Python 3.21 (see `Deprecation Timeline`_). Code that should be updated: -- ``type(ann).__name__ == '_AnnotatedAlias'`` → use - ``isinstance(ann, types.AnnotatedType)`` or - ``typing.get_origin(ann) is Annotated`` -- ``typing._AnnotatedAlias(origin, metadata)`` → use - ``Annotated[origin, *metadata]`` or ``origin @ m1 @ m2`` +- ``type(ann).__name__ == '_AnnotatedAlias'`` → use ``isinstance(ann, + types.AnnotatedType)`` or ``typing.get_origin(ann) is Annotated`` +- ``typing._AnnotatedAlias(origin, metadata)`` → use ``Annotated[origin, + *metadata]`` or ``origin @m1 @m2`` Backporting via typing_extensions ---------------------------------- -Unlike ``X | Y`` (which could be backported by ``typing_extensions`` using -``__or__``), the ``@`` shorthand requires changes to the metatype -(``type.__matmul__``), which cannot be patched from pure Python. The -shorthand is therefore only available on Python 3.16+. The existing -``Annotated[X, Y]`` syntax continues to work on all supported versions and -should be used when backwards compatibility is required. +Like ``X | Y``, the ``@`` shorthand requires changes to the metatype +(``type.__matmul__``), which cannot be patched from pure Python. The shorthand +is only available on Python 3.16+. The existing ``Annotated[X, Y]`` syntax +continues to work on all supported versions and should be used when backwards +compatibility is required. -Rejected Ideas -============== - -Mandatory List Variant ----------------------- +Security Implications +===================== -The syntax ``Type @ [ann1, ann2]`` was considered to group metadata and avoid -chaining ambiguities. While clearer in some contexts, it was deprioritized in -favor of the cleaner ``Type @ ann1 @ ann2``. +There are no direct security implications. -List-based syntax ------------------ - -An alternative syntax using list literals, such as ``[int, Metadata]``, was -rejected due to runtime semantics. In Python, a list literal evaluates to a -mutable ``list`` instance. Allowing lists as type annotations would break the -assumption of runtime checkers (like Pydantic) that annotations evaluate to -valid type constructs or ``GenericAlias`` objects, not arbitrary data -structures. +How to Teach This +================= -Scientific Computing Conflict ------------------------------ +In Python, the ``@`` symbol already has an established association with metadata +through decorators. The annotation shorthand extends this intuition to the type +system: ``int @Field(gt=0)`` reads as "``int``, decorated with ``Field(gt=0)``." -Critics note that ``ndarray @ Metadata`` visually resembles matrix -multiplication on a type whose instances are heavily associated with that -operation. However, the ``@`` -operator distinguishes between **type objects** and **instances**: ``ndarray`` -(the class) appearing in a type annotation is a type object, and ``@`` -produces an ``AnnotatedType``. An ``ndarray`` instance appearing in an -expression still uses NumPy's ``__matmul__`` for matrix multiplication. +For beginners, the key rule is: **in a type annotation, ``@`` means "with this +metadata."** For experienced developers, the mental model maps directly to +standard Python operator precedence (``@`` binds tighter than ``|``). -Since type annotations and arithmetic expressions occupy distinct syntactic -positions, this is a visual concern rather than a runtime conflict. +Documentation and teaching materials should introduce the shorthand as the +primary syntax for applying metadata. The verbose ``typing.Annotated`` form +should be treated as an advanced detail, primarily relevant to library authors +or when dynamically generating types. -Divergence from Type Theory ---------------------------- +Visual Style +------------ -Unlike ``Union`` or ``Generics``, using an operator for metadata is a -Python-specific ergonomic choice rather than a standard type-theoretic -construct. This follows the pragmatic precedent of :pep:`604`. +The shorthand must be strictly formatted as ``type @annot`` (e.g., ``int +@Metadata(...)``), with a space before the ``@`` and no space after it. This +distinguishes it from standard matrix multiplication (``A @ B``) and aligns +visually with function decorators (``@decorator``) and Java annotations. Code +formatters (like Ruff and Black) should enforce this spacing within typing +contexts. Usage Examples ============== @@ -421,15 +468,15 @@ Usage Examples Pydantic Validation ------------------- -The shorthand excels in data validation scenarios:: +The shorthand can be used in data validation scenarios:: from pydantic import BaseModel, Field, HttpUrl from annotated_types import Len class Project(BaseModel): - name: str @ Field(title="Project Name") @ Len(1) - url: HttpUrl @ Field(description="The project homepage") - stars: int @ Field(ge=0) = 0 + name: str @Field(title="Project Name") @Len(1) + url: HttpUrl @Field(description="The project homepage") + stars: int @Field(ge=0) = 0 FastAPI Dependency Injection ---------------------------- @@ -441,7 +488,7 @@ In FastAPI, the shorthand simplifies complex parameter definitions:: app = FastAPI() @app.get("/secure") - async def secure_endpoint(token: str @ Header(description="Authentication token")): + async def secure_endpoint(token: str @Header(description="Auth token")): return {"status": "authorized"} SQLModel and Database Definitions @@ -453,36 +500,280 @@ shorthand syntax makes these definitions significantly cleaner:: from sqlmodel import SQLModel, Field class Hero(SQLModel, table=True): - id: (int | None) @ Field(primary_key=True) = None - name: str @ Field(index=True) + id: (int | None) @Field(primary_key=True) = None + name: str @Field(index=True) secret_name: str - age: (int | None) @ Field(index=True) = None + age: (int | None) @Field(index=True) = None + +Testing and Formal Verification +------------------------------- + +Libraries like Hypothesis (property-based testing) and CrossHair (symbolic +execution) utilize ``annotated-types`` to constrain test generation and +analysis. The shorthand provides a clean syntax for specifying test boundaries:: + + from dataclasses import dataclass + from annotated_types import Ge, Interval + from hypothesis import given + + @dataclass + class InventoryItem: + # A non-negative quantity + quantity: int @Ge(0) + # A price bounded between 1 and 100 + price: float @Interval(gt=0, le=100) + + @given(...) + def test_inventory(item: InventoryItem): + assert item.price * item.quantity >= 0 Reference Implementation ======================== Prototype implementations are available for the following tools: -- **CPython:** `CPython at-type-annot `_ -- **CPython (with annotation-lib structural forward references):** `CPython forward-stringifier `_ -- **Mypy:** `Mypy at-type-annot `_ -- **Mypyc/ast_serialize:** `ast_serialize at-type-annot `_ -- **Pyright:** `Pyright at-type-annot `_ -- **Ruff:** `Ruff at-type-annot `_ +- **CPython:** `CPython at-type-annot + `_ +- **CPython (with annotation-lib structural forward references):** `CPython + forward-stringifier + `_ +- **Mypy:** `Mypy at-type-annot + `_ +- **Mypyc/ast_serialize:** `ast_serialize at-type-annot + `_ +- **Pyright:** `Pyright at-type-annot + `_ +- **Ruff:** `Ruff at-type-annot + `_ + +``builtins.type`` in Typeshed will be updated to include ``def +__matmul__(self, other: Any) -> types.AnnotatedType: ...`` to support type +checkers. + +Rejected Ideas +============== + +Alternative Syntaxes +-------------------- + +Debates around reusing ``@`` yielded several alternatives: + +- A new infix operator: ``int <@ Field(...)`` +- A new soft keyword: ``int annotated Interval(1, 10)`` +- Bracket syntax: ``x: int {Gt(10), Lt(20)}`` + +These were rejected because none garnered wide consensus. The ``@`` symbol also +extends cleanly to Symbol Decorators should the language pursue that route in +the future. + +Reliance on ``__rmatmul__`` +--------------------------- + +We explicitly reject relying on ``__rmatmul__`` on the metadata objects. One +might envision a base class that implements ``__rmatmul__`` to return an +``Annotated`` type:: + + class Metadata[T = object]: + def __rmatmul__(self, typ: TypeForm[T], /) -> TypeForm[T]: + return Annotated[typ, self] + + class Le(Metadata[int]): + ... + +This fails for two reasons. First, Type Expressions require fixed semantics; +relying on arbitrary right-hand objects to implement ``__rmatmul__`` fragments +the grammar and is inconsistent with how ``|`` works. Second, :pep:`593` +explicitly permits any valid Python object as metadata (e.g., strings, dicts). +Modifying the ``type`` metaclass's ``__matmul__`` guarantees consistent behavior +for all metadata without requiring opt-in base classes. + +Type Expressions vs. Non-Typing Annotations +------------------------------------------- + +The boundary between Type Expressions and Non-Typing Annotations is reinforced, +aligning with the formal typing specification. + +Parameter/Field Decorators +-------------------------- + +True parameter/field decorators were rejected for now. Modifying the core +parser to support symbol-level decorators opens a complex design space. This +is strictly scoped to type-level decorations. + +Existing Custom ``@`` Usage +--------------------------- + +Frameworks that currently rely on a custom ``@`` operator in annotations are +violating the typing specification. These are considered Non-Typing +Annotations. To clarify this boundary, ``Format.TYPE`` explicitly does not +handle Non-Typing Annotations. They remain fully supported via ``Format.STRING`` +or ``Format.VALUE`` when wrapped in ``@no_type_check``, and we do not break +metaclasses that implement their own ``__matmul__``. + +Operator Overloading +-------------------- + +Using ``@`` might confuse users expecting matrix multiplication. However, within +a Type Expression, Python intentionally reuses standard operators (like ``|`` +and ``[]``) with typing-specific semantics. + +Format.TYPE Coupling +-------------------- + +The coupling of ``Format.TYPE`` with Type Expression semantics is intentional +and reinforces the boundary between typing evaluation and normal runtime +evaluation. For example, ``NoneType`` explicitly does not implement +``__matmul__`` to prevent masking runtime bugs. Because ``Format.TYPE`` uses +typing semantics, it can safely evaluate ``None @Metadata`` structurally without +altering global ``NoneType`` behavior. + +Future Work +=========== + +While this feature stands on its own, establishing ``@`` as a standard metadata +token opens the door to several speculative extensions. The following ideas +outline one possible direction the ecosystem could take. + +Native Symbol Decorators +------------------------ + +Developers frequently request framework-agnostic field decorators:: + + class User(BaseModel): + @Field(primary_key=True) + id: int + +Reserving ``@`` for metadata provides the grammatical foundation for Symbol +Decorators. Any future proposal must choose between two architectural models: + +- **The Descriptor Model (Active Runtime):** The decorator acts as a runtime + function returning a `descriptor `__, + actively intercepting attribute access (similar to standard Python function + decorators). +- **The Metadata Model (Passive Declaration):** The field configuration is + treated as passive Symbol Metadata, leaving the enclosing class to process it + during creation. + +This PEP anchors Python to the Metadata Model. The modern ecosystem is heavily +driven by type-directed libraries (Pydantic, SQLAlchemy, ``dataclasses``) that +inspect static definitions during class creation rather than relying on +standalone descriptors. + +Because we are establishing the Metadata Model, ``@Field(...) id: int`` will +evaluate identically to ``id: int @Field(...)``. This alignment allows these +frameworks to inspect field configurations and continue working without +requiring extensive library changes. + +.. note:: + This resolves the conceptual tension between value space and type space. + While standard decorators operate in value space (actively modifying runtime + objects), this syntax firmly establishes ``@`` in type space, safely + attaching Symbol Metadata without altering runtime execution. + +Targeted Metadata +----------------- + +Future extensions to :pep:`746` will support annotation targets. By intersecting +a base type with an explicit target constraint, type checkers will validate +*where* metadata is allowed to exist. This prevents misuse (e.g., placing a +``@Column`` on a function parameter rather than a class field): + +*(Note: The following example assumes the ``&`` operator for intersection types +has been added.)* + +:: + + from typing import Target + + class Column: + """Valid only on integers that are fields of a SQLAlchemy Model.""" + __supports_annotated_base__: int & Target.FIELD[SQLAlchemy.Model] + +``Annotated`` has become a foundational building block for modern Python +frameworks. By establishing a clean, native syntax for metadata, this preserves +readability while paving the way for more sophisticated analysis tools and a +richer typing ecosystem. + +Open Issues +=========== + +Deprecation Timeline +-------------------- + +The removal of the ``typing._AnnotatedAlias`` compatibility layer is scheduled +for Python 3.21, following the standard 5-year deprecation policy (:pep:`387`). +However, given the widespread use of ``typing.Annotated``, the exact timeline +for removal (or whether the shim should be retained indefinitely to prevent +churn in legacy code) remains open for community discussion. + +``annotationlib.Format.TYPE`` Extraction +---------------------------------------- + +``Format.TYPE`` enables the structural evaluation of Type Expressions. While +required here to support ``type @annot``, it fundamentally improves the +evaluation of type expressions (such as correctly resolving ``|`` unions that +would otherwise be stringified by ``Format.FORWARDREF``). We invite discussion +on whether this broader semantic addition to ``annotationlib`` warrants +extraction into an independent PEP. + +Acknowledgements +================ + +Thanks to Hugo van Kemenade, Jelle Zijlstra, and Eric Traut for their feedback, +guidance, and assistance in refining this proposal. References ========== -- `Discussion on Python Discourse `_ -- :pep:`563` -- Postponed evaluation of annotations -- :pep:`585` -- Type hinting generics in standard collections -- :pep:`593` -- Flexible function and variable annotations -- :pep:`604` -- Allow writing union types as ``X | Y`` -- :pep:`727` -- Documentation metadata in typing (Withdrawn) -- :pep:`749` -- Implementing PEP 649 +- `Discussion on Python Discourse `_ + +.. [1] *"Functions where the arguments have type annotations can already be + rather long, and Annotated on its own is rather verbose, so I’m generally + glad it’s rare"* — Paul Moore on PEP 727: + https://discuss.python.org/t/32566/17 +.. [2] FastAPI support for ``Annotated``: + https://fastapi.tiangolo.com/tutorial/query-params-str-validations/ +.. [3] Pydantic support for ``Annotated``: + https://docs.pydantic.dev/latest/concepts/types/#annotated-types +.. [4] cattrs support for ``Annotated``: + https://cattrs.readthedocs.io/en/latest/validation.html#annotated +.. [5] msgspec support for ``Annotated``: + https://jcristharif.com/msgspec/supported-types.html#annotated +.. [6] SQLAlchemy 2.0 support for ``Annotated``: + https://docs.sqlalchemy.org/en/20/orm/declarative_tables.html#using-annotated-declarative-table-type-annotated-forms-for-mapped-column +.. [7] Typer support for ``Annotated``: + https://typer.tiangolo.com/tutorial/parameter-types/annotated/ +.. [8] Beartype support for ``Annotated`` (Validators): + https://beartype.readthedocs.io/en/latest/api_vale/ +.. [9] *"Annotated syntax is too long: Introduction of Annotated params made + function params more logical, but on the other hand longer/more verbose"* — + Vitaliy Kucheryaviy, author of Django Ninja: + https://github.com/tiangolo/fastapi/discussions/10055#discussion-5507018 +.. [10] *"I personally find this solution [using Annotated] a bit tedious when + you start having a lot of models/fields"* — g0di, Pydantic user: + https://github.com/pydantic/pydantic/discussions/2419#discussioncomment-7228409 +.. [11] SQLAlchemy 2.0 Migration Guide (advocating Annotated aliases to + mitigate verbosity): + https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#step-five-make-use-of-pep-593-annotated-to-package-common-directives-into-types +.. [12] *"My main concern here is that Annotated[torch.Tensor, dtype] is quite + verbose, and seems to go in the opposite direction to where we'd like to end + up"* — Ralf Gommers, NumPy maintainer: + https://github.com/pytorch/pytorch/issues/98702#issuecomment-1504794519 +.. [13] *"I'm not writing something stupidly verbose like: TensorBatchXChannels + = Annotated[...]"* — Patrick Kidger, author of TorchTyping: + https://github.com/beartype/beartype/discussions/96#discussioncomment-2245014 +.. [14] 2023 Python Discourse discussion proposing ``@`` as an alternative to + Annotated: + https://discuss.python.org/t/40751 +.. [15] 2025 Python Discourse discussion converging on dedicated ``@`` syntax: + https://discuss.python.org/t/103699 +.. [16] *"That by itself doesn’t seem a big objection – type annotations reuse + all kinds of operations, including x[y] and x | y."* — Guido van Rossum, on + repurposing the @ operator for typing: + https://discuss.python.org/t/40751/3 Copyright ========= -This document is placed in the public domain or under the -CC0-1.0-Universal license, whichever is more permissive. +This document is placed in the public domain or under the CC0-1.0-Universal +license, whichever is more permissive. From b644885bc74af9123530c3992b0b361ac39c8a93 Mon Sep 17 00:00:00 2001 From: Till Varoquaux Date: Thu, 9 Jul 2026 01:34:08 -0400 Subject: [PATCH 2/2] PEP 835: Trim redundancy and tighten narrative --- peps/pep-0835.rst | 452 ++++++++++++++++++---------------------------- 1 file changed, 175 insertions(+), 277 deletions(-) diff --git a/peps/pep-0835.rst b/peps/pep-0835.rst index 64574ae08fc..0b0b55aef2d 100644 --- a/peps/pep-0835.rst +++ b/peps/pep-0835.rst @@ -14,54 +14,38 @@ Post-History: `19-Apr-2026 `__ using the -``@`` operator. This change improves the ergonomics of type constraints and -reduces signature verbosity, benefiting type-directed libraries like -Pydantic, FastAPI, Typer, and SQLModel. +This PEP proposes overloading the ``@`` operator on types to allow writing +``Annotated[T, M]`` as ``T @M``. Motivation ========== -Metadata as a Semantic Operation --------------------------------- - -Historically, Python has treated type metadata as an afterthought. -``Annotated[T, M]`` models metadata as a generic container, similar to how -``Union[X, Y]`` once modeled unions. But metadata is not a container; it is a -modifier applied to a type. - -Just as :pep:`604` evolved unions from the ``Union`` container to the ``|`` -operator, the ``@`` syntax evolves metadata into a native language operator. -Elevating metadata to first-class syntax recognizes that modern libraries -increasingly use constraints on types (e.g., bounds, shapes, patterns). +Modern Python libraries increasingly rely on type metadata for +meta-programming. Adding this metadata has historically forced developers to +choose between static analysis and readability. -Previously, applying these constraints forced a frustrating compromise between -type safety and readability. Before :pep:`593` (``Annotated``), frameworks like -Pydantic and FastAPI embedded metadata directly into default values. This was -concise but broke static analysis: +Before :pep:`593` (``Annotated``), frameworks embedded metadata into default +values, breaking type checkers: .. code-block:: :class: bad class User(BaseModel): - id: int = Field(gt=0) - name: str = Field(min_length=3) + id: int = Field(gt=0, le=1000) -Moving to ``Annotated`` fixed the typing semantics but introduced significant -complexity. To hide the visual noise, libraries introduced alias types (e.g., -``PositiveInt``). However, the moment users step outside pre-packaged -constraints, they are forced back into the complex syntax:: +``Annotated`` fixed the typing semantics but introduced deep nesting. To manage +this overhead, libraries often provide alias types (e.g., ``PositiveInt``). +Custom constraints still require the nested syntax:: from typing import Annotated from pydantic import BaseModel, Field, PositiveInt class User(BaseModel): - age: PositiveInt # Concise but limited + age: PositiveInt # Concise but limited alias id: Annotated[int, Field(gt=0, le=1000)] # Verbose fallback -The ``@`` shorthand resolves the ergonomic tradeoff, restoring the conciseness -of early frameworks while preserving the strict semantics of ``Annotated``: +The ``@`` shorthand removes this tradeoff, combining the readability of early +frameworks with the strict semantics of ``Annotated``: .. code-block:: :class: good @@ -78,23 +62,23 @@ of early frameworks while preserving the strict semantics of ``Annotated``: async def read_items(q: (str | None) @Query(max_length=50) = None): ... +``x: Annotated[str, ...]`` obscures a value's runtime type by burying it inside +brackets. The ``x: str @Annotation`` syntax keeps it front-and-center. + Prior Art and Historical Context -------------------------------- -The verbosity of ``Annotated`` is not just an aesthetic annoyance; it hinders -adoption. During the 2023 review of :pep:`727`, reviewers warned that forcing -developers to use ``Annotated`` for everyday documentation would introduce -excessive visual noise [1]_. This pushback led to the PEP's withdrawal and -sparked the initial discussions around using ``@`` as a native alternative. - -Today, this friction remains evident across the entire ecosystem. The most -prominent type-directed frameworks in Python (FastAPI, Pydantic, SQLAlchemy, -cattrs, msgspec, Typer, and beartype) all heavily rely on ``Annotated`` [2]_ -[3]_ [4]_ [5]_ [6]_ [7]_ [8]_. Yet, they frequently receive user complaints -about signature bloat [9]_ [10]_ [11]_. In some domains, library authors refuse -to use it entirely: PyTorch [12]_, TorchTyping, and Beartype [13]_ rejected -``Annotated`` for tensor shape typing, deciding instead to wait for a native -language solution. +The syntactic overhead of ``Annotated`` hinders adoption. During the 2023 +discussions of :pep:`727`, participants worried that relying on ``Annotated`` +for everyday documentation would harm readability [1]_. This feedback led to +the PEP's withdrawal and initiated discussions for a dedicated ``@`` operator. + +Major type-directed frameworks (FastAPI, Pydantic, SQLAlchemy, cattrs, msgspec, +Typer, beartype) rely heavily on ``Annotated`` [2]_ [3]_ [4]_ [5]_ [6]_ [7]_ +[8]_. As a result, users frequently complain about its verbosity [9]_ +[10]_ [11]_. In some domains, authors explicitly defer adopting it: PyTorch +[12]_, TorchTyping, and Beartype [13]_ rejected ``Annotated`` for tensor +shapes, waiting instead for a native syntax. When the community debated alternatives in late 2023 [14]_ and 2025 [15]_, discussion trended toward the ``@`` operator. It visually aligns with existing @@ -106,7 +90,7 @@ operators for static typing, as seen with ``[]`` for generics (:pep:`585`) and Precedent in Other Languages ---------------------------- -The ``@`` metadata syntax mirrors features in other statically typed languages: +The ``@`` syntax mirrors features in other statically typed languages: - **C++:** Uses ``[[attribute]]`` for both symbols and types (e.g., ``[[nodiscard]] int f();``). @@ -125,43 +109,41 @@ Terminology To clarify discussion around annotations and metadata, the following terms apply: -- **Type Expression**: e.g., ``x: ``. Expressions evaluating to valid - static types, as specified in the `Typing Specification +- **Type expression**: e.g., ``x: ``. An expression within a type hint + that evaluates to a valid type, as specified in the `Typing Specification `__ (:pep:`484`). -- **Type Metadata**: e.g., ``int @``. Data attached to a Type Expression +- **Type metadata**: e.g., ``int @``. Data attached to a type expression via ``Annotated`` (:pep:`593`, :pep:`746`). -- **Non-Typing Annotation**: e.g., `@no_type_check +- **Non-typing annotation**: e.g., `@no_type_check `__ - wrapping ``x: ``. - Standard behavior where annotations are evaluated as arbitrary runtime values - rather than strict type hints. -- **Symbol Decorator**: Applying metadata directly to a variable or field - declaration, rather than to its underlying type. While Python does not - currently have a native syntax for variable decorators, this conceptual - distinction is well-established in languages like Java: + wrapping ``x: ``. Annotations that instruct static type checkers that + the expression is not a type expression and should be ignored. +- **Symbol decorator**: Applying metadata directly to a variable or field + declaration, rather than to its underlying type. This conceptual distinction + is well-established in languages like Java: .. code-block:: java class Application { - // Field Decorator (Symbol Decorator) + // Field Decorator (symbol decorator) @Inject private Service s; - // Type Decorator (Type Metadata) + // Type Decorator (type metadata) private @NonNull String name; } -- **Symbol Metadata** (or **Field Metadata**): Data attached to a symbol via a - Symbol Decorator. This contrasts with an active runtime descriptor. It is - fundamentally distinct from Type Metadata, as it applies to the specific +- **Symbol metadata** (or **field metadata**): Data attached to a symbol via a + symbol decorator. This contrasts with an active runtime descriptor. It is + distinct from type metadata, as it applies to the specific instance of the variable rather than the type itself. Specification ============= -The proposed syntax uses the ``@`` (``__matmul__``) operator to attach metadata -to a type:: +The proposed syntax uses the `__matmul__` (`@`) operator to attach metadata to a +type:: # Current syntax x: Annotated[int, Range(0, 10)] @@ -172,13 +154,10 @@ to a type:: Operator Precedence ------------------- -The ``@`` operator binds tighter than the ``|`` union operator (:pep:`604`). -This aligns with standard Python expression precedence, ensuring predictable -parsing for Type Metadata. - -Because ``@`` binds tightly, developers can attach distinct constraints directly -to specific types within a union. For example, a US zip code might be a 5-digit -integer *or* a 5-character string:: +Because ``@`` binds tighter than the ``|`` union operator (:pep:`604`), +distinct constraints can be attached directly to specific types within a union +without parentheses. For example, a US zip code might be a 5-digit integer +*or* a 5-character string:: zip_code: int @Ge(10000) @Le(99999) | str @Len(5) @@ -190,41 +169,31 @@ If you intend to attach metadata to the entire union, you must use parentheses:: # Attaches to the entire union (int | str) @Metadata # equivalent to: Annotated[int | str, Metadata] -The "Parentheses Friction" -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Some developers have noted that writing ``(str | None) @Field(...)`` feels -cumbersome for optional fields. -This awkwardness highlights a missing language feature. Without native Symbol -Decorators, frameworks must co-opt ``Annotated`` to configure fields. When a -developer writes ``address: (str | None) @Field(...)``, they are actually -attempting to configure the ``address`` field, not the ``str | None`` -type. Until native field decorators exist, parentheses remain the structurally -accurate way to express this in the type system. +Writing ``(str | None) @Field(...)`` can feel cumbersome for optional fields. +This stems from a missing language feature. Without native symbol decorators, +frameworks must co-opt ``Annotated`` to configure fields. A developer writing +``address: (str | None) @Field(...)`` intends to configure the ``address`` +field, not the ``str | None`` type. Flattening Multiple Metadata ---------------------------- -When multiple metadata items are chained, the resulting ``Annotated`` object is -flattened. - -Specifically, ``T @m1 @m2`` evaluates to ``Annotated[T, m1, m2]``. It must not -resolve to a nested structure such as ``Annotated[Annotated[T, m1], m2]``. This -mirrors the existing runtime behavior of ``typing.Annotated``. +Chained metadata flattens the resulting ``Annotated`` object. ``T @m1 @m2`` +evaluates to ``Annotated[T, m1, m2]``, never ``Annotated[Annotated[T, m1], +m2]``. This mirrors ``typing.Annotated``'s existing runtime behavior. -This flattening also applies when the left-hand operand is an existing -``Annotated`` type, regardless of how it was constructed:: +Flattening also applies when the left-hand operand is an existing ``Annotated`` +type:: Annotated[int, m1] @m2 # AnnotatedType(int, m1, m2) (flattened) Runtime Behavior ---------------- -The ``@`` operator produces a ``types.AnnotatedType`` instance, a new built-in -type implemented in C. The existing ``typing.Annotated`` is unified with this -type: ``typing.Annotated[X, Y]`` returns the same ``types.AnnotatedType`` object -as ``X @Y``: +``@`` produces a ``types.AnnotatedType`` (a new built-in C type). The existing +``typing.Annotated`` unifies with this type. ``typing.Annotated[X, Y]`` and ``X +@ Y`` return the exact same object: .. code-block:: pycon @@ -248,14 +217,11 @@ The ``repr()`` of an ``AnnotatedType`` uses the shorthand syntax: >>> int @Field(gt=0) int @Field(gt=0) -``AnnotatedType`` objects support pickling via ``copyreg``, reconstructing -through ``AnnotatedType[origin, *metadata]``. - Handling of ``None`` -------------------- -Implementing ``__matmul__`` on ``NoneType`` is explicitly avoided to prevent -masking runtime bugs in non-typing contexts. +``NoneType`` explicitly avoids implementing ``__matmul__`` to prevent masking +runtime bugs. For example, a developer might forget to validate ``None`` before executing matrix multiplication on an array: @@ -266,48 +232,37 @@ matrix multiplication on an array: def matmul_arrays(a: np.array | None, b: np.array): return a @ b # oops, forgot to check for None -If ``__matmul__`` were implemented on ``NoneType``, this would return an -``AnnotatedType`` object instead of raising a ``TypeError``, silently masking -the bug. +If ``NoneType.__matmul__`` existed, this would silently return an +``AnnotatedType`` instead of raising a ``TypeError``. -Because ``Format.TYPE`` is introduced to ``annotationlib``, this limitation is -invisible in valid typing contexts. ``Format.TYPE`` evaluates -structurally and correctly parses ``None @Metadata`` into an ``AnnotatedType`` -without relying on ``NoneType.__matmul__``. Outside of Type Expressions -(e.g., in raw runtime execution), users must use ``Annotated[None, Metadata]``. +``annotationlib.Format.TYPE`` makes this limitation invisible in valid typing +contexts. It evaluates structurally, correctly parsing ``None @Metadata`` into +an ``AnnotatedType`` without calling ``NoneType.__matmul__``. Outside of type +expressions, users must fall back to ``Annotated[None, Metadata]``. Supported Left-Hand Operands ----------------------------- -The ``@`` operator is implemented by adding ``nb_matrix_multiply`` to the -metatype (``type``) and to exactly the same typing constructs that currently -support the ``|`` operator for unions (``types.GenericAlias``, +The ``@`` operator adds ``nb_matrix_multiply`` to ``type`` and to all typing +constructs that support the ``|`` union operator (``types.GenericAlias``, ``types.UnionType``, ``types.AnnotatedType``, ``typing.TypeVar``, ``typing.ParamSpec``, ``typing.TypeVarTuple``, ``typing.TypeAliasType``, ``typing.ForwardRef``, and ``sentinel`` objects). -For all other left-hand operands, the operator returns ``NotImplemented``, -allowing normal ``__matmul__`` dispatch to proceed. - -This means ``int @Field()`` produces an ``AnnotatedType``, while ``42 -@something`` is unaffected. Crucially, ``ndarray @Field()`` (using the **class** -as a type annotation) also produces an ``AnnotatedType``, even though ndarray -*instances* define ``__matmul__`` for matrix multiplication. One consequence of -this design is that applying the ``@`` operator to a class object evaluates as -Type Metadata, while applying it to an instance performs arithmetic. - -The only case where the shorthand does not apply is when a class has a -**metaclass** that defines ``__matmul__``. In that case, the metaclass's -operator takes priority via standard Python MRO dispatch. - +``int @Field()`` produces an ``AnnotatedType``, while ``42 @something`` raises a +TypeError (or delegates to ``__rmatmul__`` on ``something``). ``ndarray +@Field()`` produces an ``AnnotatedType``, even though ``ndarray`` instances +define ``__matmul__``. Applying ``@`` to a class evaluates as type metadata; +applying it to an instance performs arithmetic. +Custom metaclasses can still overload ``__matmul__`` as long as ``@`` is avoided +in type expressions. Forward References and Deferred Evaluation ------------------------------------------- Under :pep:`749`'s lazy evaluation, existing ``annotationlib`` formats are -insufficient for ``@``. Specifically, ``Format.FORWARDREF`` fails structurally -on unresolvable names:: +insufficient for ``@``. ``Format.FORWARDREF`` stringifies unresolvable names:: class Model: ref: NotYetDefined @Field(gt=0) @@ -316,49 +271,34 @@ on unresolvable names:: @Field(gt=0)')``. The metadata is trapped inside the unresolved string, blocking libraries like Pydantic from inspecting it at class-definition time. -``Format.TYPE`` assumes typing semantics and evaluates Type Expressions +``Format.TYPE`` assumes typing semantics and evaluates type expressions structurally. Unresolvable names are wrapped in a ``ForwardRef`` independently, leaving operators intact:: AnnotatedType(ForwardRef('NotYetDefined'), Field(gt=0)) The metadata remains immediately accessible. This also resolves the pre-existing -issue where ``"Foo" | int`` incorrectly produced ``ForwardRef('Foo | int')`` -under ``Format.FORWARDREF``. - -Importantly, because ``Format.TYPE`` can evaluate structurally when runtime -dunder methods are missing (falling back to ``types.UnionType`` and -``types.AnnotatedType`` for the ``|`` and ``@`` operators), it parses ``None -@Metadata`` without requiring ``NoneType.__matmul__``. - -Parsing and Grammar -------------------- - -No changes to the Python grammar are required. Because ``@`` is already a valid -operator, it is natively parsed as a binary operation. The shorthand is resolved -during semantic analysis, entirely bypassing the need to patch grammar files or -update the parser. +limitation where ``"Foo" | int`` produced ``ForwardRef('Foo | int')`` under +``Format.FORWARDREF``. Rationale ========= -Baking ``Annotated`` into native syntax removes ``typing`` import overhead, -directly aiding runtime type introspection. - -Because ``@`` is already a valid expression operator, type checkers (Mypy, -Pyright) require no parser changes, handling the syntax entirely during semantic -analysis. We have prototyped an automated conversion rule in Ruff, enabling -automated codebase migrations, and CPython prototype testing confirms that -libraries like ``typer`` and ``pydantic`` work natively out of the box. +The ``@`` shorthand doesn't require changes to the Python parser. Because ``@`` +is already a valid operator, the grammar remains unchanged. Type checkers (Mypy, +Pyright) handle the syntax entirely during semantic analysis. We prototyped a +Ruff conversion rule for automated migrations, and CPython prototype testing +confirms that libraries like ``typer`` and ``pydantic`` continue to work without +modification. Why the @ Operator? ------------------- -Selecting the correct operator for metadata involves balancing three grammatical +Selecting the correct operator for metadata involves balancing three considerations: -1. **Precedence & Spec Restrictions:** The typing specification currently only - allows the ``|`` operator in Type Expressions. Any new operator must bind +1. **Precedence & Spec Restrictions:** The typing specification only + permits operators to be used within type expressions if they bind tighter than ``|`` (Union) and ``&`` (proposed Intersection) so that expressions like ``int @Field() | str`` parse correctly without parentheses. This eliminates operators like ``|``, ``^``, and ``&``. @@ -368,44 +308,38 @@ considerations: aligns with the Python typing specification's precedent of preferring operators (e.g., ``|``) over keywords within type expressions. 3. **Semantic Clarity:** The chosen syntax should avoid colliding with - established mathematical intuitions for primitive types. + established intuitions for primitive types. This leaves the set of overridable binary operators that bind tighter than ``&``: ``**``, ``*``, ``@``, ``/``, ``//``, ``%``, ``+``, ``-``, ``>>``, and ``<<``. Standard arithmetic operators like ``+``, ``-``, ``/``, ``//``, ``*``, ``**``, -and ``%`` are misleading in this context. Reading ``int + x`` or ``float / -Field()`` strongly implies mathematical evaluation, not metadata decoration. +and ``%`` are misleading. Reading ``int + x`` or ``float / Field()`` strongly +implies mathematical evaluation, not metadata decoration. This leaves ``<<``, ``>>``, and ``@``. Of these, ``@`` is the only operator that possesses an existing association with metadata in Python (via function and -class decorators). Bitwise shift operators (``<<``, ``>>``) lack this -association. +class decorators). -While ``@`` is used for matrix multiplication in numeric libraries (like NumPy), -it is far less associated with core scalar arithmetic than operators like ``+`` -or ``/``. +While ``@`` is used for matrix multiplication in external libraries (like +NumPy), it is far less associated with core scalar arithmetic than operators +like ``+`` or ``/``. Language Complexity ------------------- -Adding new syntax always introduces some language complexity. However, while -syntactic complexity increases, the *cognitive* load actually decreases. +While adding syntax increases structural complexity, the *cognitive* load +decreases. -Teaching a beginner ``int @Field(gt=0)`` leverages their existing intuition of -Python decorators. It visually separates the base type from its metadata. -Teaching ``typing.Annotated`` requires referencing obscure parts of the typing -module. By moving metadata from a standard library class to a native grammatical -construct, we reduce the overall cognitive load required to read and write -modern typed Python. +Teaching a beginner ``int @Field(gt=0)`` leverages their intuition of +decorators. It visually separates the base type from its metadata. Teaching +``typing.Annotated`` requires referencing obscure parts of the typing +module. This reduces the cognitive overhead when using type-directed libraries. Backwards Compatibility ======================= -typing.Annotated Migration ---------------------------- - The pure-Python ``typing._AnnotatedAlias`` class is replaced with a native C implementation (``types.AnnotatedType``). ``typing.Annotated`` becomes a reference to this C type rather than a special form with a custom metaclass. @@ -413,7 +347,7 @@ reference to this C type rather than a special form with a custom metaclass. The private ``typing._AnnotatedAlias`` class is retained as a deprecated compatibility shim. Code using ``isinstance(x, typing._AnnotatedAlias)`` will continue to work but emit a ``DeprecationWarning``. The shim is scheduled for -removal in Python 3.21 (see `Deprecation Timeline`_). +removal in Python 3.21 (see `Open Issues`_). Code that should be updated: @@ -422,9 +356,7 @@ Code that should be updated: - ``typing._AnnotatedAlias(origin, metadata)`` → use ``Annotated[origin, *metadata]`` or ``origin @m1 @m2`` -Backporting via typing_extensions ----------------------------------- - +**Backporting via typing_extensions:** Like ``X | Y``, the ``@`` shorthand requires changes to the metatype (``type.__matmul__``), which cannot be patched from pure Python. The shorthand is only available on Python 3.16+. The existing ``Annotated[X, Y]`` syntax @@ -452,23 +384,18 @@ primary syntax for applying metadata. The verbose ``typing.Annotated`` form should be treated as an advanced detail, primarily relevant to library authors or when dynamically generating types. -Visual Style ------------- - -The shorthand must be strictly formatted as ``type @annot`` (e.g., ``int -@Metadata(...)``), with a space before the ``@`` and no space after it. This -distinguishes it from standard matrix multiplication (``A @ B``) and aligns -visually with function decorators (``@decorator``) and Java annotations. Code -formatters (like Ruff and Black) should enforce this spacing within typing -contexts. +**Visual Style:** The shorthand must be formatted as ``type @annot`` (e.g., +``int @Metadata(...)``), with a space before the ``@`` and no space after +it. This distinguishes it from standard matrix multiplication (``A @ B``) and +aligns visually with function decorators (``@decorator``) and Java +annotations. Code formatters (like Ruff and Black) should enforce this spacing +within typing contexts. Usage Examples ============== -Pydantic Validation -------------------- - -The shorthand can be used in data validation scenarios:: +**Pydantic Validation:** The shorthand can be used in data validation +scenarios:: from pydantic import BaseModel, Field, HttpUrl from annotated_types import Len @@ -478,10 +405,8 @@ The shorthand can be used in data validation scenarios:: url: HttpUrl @Field(description="The project homepage") stars: int @Field(ge=0) = 0 -FastAPI Dependency Injection ----------------------------- - -In FastAPI, the shorthand simplifies complex parameter definitions:: +**FastAPI Dependency Injection:** In FastAPI, the shorthand simplifies complex +parameter definitions:: from fastapi import FastAPI, Header, Depends @@ -491,11 +416,9 @@ In FastAPI, the shorthand simplifies complex parameter definitions:: async def secure_endpoint(token: str @Header(description="Auth token")): return {"status": "authorized"} -SQLModel and Database Definitions ---------------------------------- - -SQLModel relies heavily on ``Annotated`` to define column properties. The -shorthand syntax makes these definitions significantly cleaner:: +**SQLModel and Database Definitions:** SQLModel relies heavily on +``Annotated`` to define column properties. The +shorthand syntax makes these definitions cleaner:: from sqlmodel import SQLModel, Field @@ -505,12 +428,10 @@ shorthand syntax makes these definitions significantly cleaner:: secret_name: str age: (int | None) @Field(index=True) = None -Testing and Formal Verification -------------------------------- - -Libraries like Hypothesis (property-based testing) and CrossHair (symbolic -execution) utilize ``annotated-types`` to constrain test generation and -analysis. The shorthand provides a clean syntax for specifying test boundaries:: +**Testing and Formal Verification:** Libraries like Hypothesis (property-based +testing) and CrossHair (symbolic execution) utilize ``annotated-types`` to +constrain test generation and analysis. The shorthand provides a clean syntax +for specifying test boundaries:: from dataclasses import dataclass from annotated_types import Ge, Interval @@ -563,15 +484,14 @@ Debates around reusing ``@`` yielded several alternatives: - Bracket syntax: ``x: int {Gt(10), Lt(20)}`` These were rejected because none garnered wide consensus. The ``@`` symbol also -extends cleanly to Symbol Decorators should the language pursue that route in +extends cleanly to symbol decorators should the language pursue that route in the future. Reliance on ``__rmatmul__`` --------------------------- -We explicitly reject relying on ``__rmatmul__`` on the metadata objects. One -might envision a base class that implements ``__rmatmul__`` to return an -``Annotated`` type:: +We explicitly reject relying on metadata objects implementing ``__rmatmul__`` +(e.g., via a base class) to return an ``Annotated`` type:: class Metadata[T = object]: def __rmatmul__(self, typ: TypeForm[T], /) -> TypeForm[T]: @@ -580,19 +500,13 @@ might envision a base class that implements ``__rmatmul__`` to return an class Le(Metadata[int]): ... -This fails for two reasons. First, Type Expressions require fixed semantics; +This fails for two reasons. First, type expressions require fixed semantics; relying on arbitrary right-hand objects to implement ``__rmatmul__`` fragments the grammar and is inconsistent with how ``|`` works. Second, :pep:`593` explicitly permits any valid Python object as metadata (e.g., strings, dicts). Modifying the ``type`` metaclass's ``__matmul__`` guarantees consistent behavior for all metadata without requiring opt-in base classes. -Type Expressions vs. Non-Typing Annotations -------------------------------------------- - -The boundary between Type Expressions and Non-Typing Annotations is reinforced, -aligning with the formal typing specification. - Parameter/Field Decorators -------------------------- @@ -600,39 +514,37 @@ True parameter/field decorators were rejected for now. Modifying the core parser to support symbol-level decorators opens a complex design space. This is strictly scoped to type-level decorations. -Existing Custom ``@`` Usage ---------------------------- - -Frameworks that currently rely on a custom ``@`` operator in annotations are -violating the typing specification. These are considered Non-Typing -Annotations. To clarify this boundary, ``Format.TYPE`` explicitly does not -handle Non-Typing Annotations. They remain fully supported via ``Format.STRING`` -or ``Format.VALUE`` when wrapped in ``@no_type_check``, and we do not break -metaclasses that implement their own ``__matmul__``. - Operator Overloading -------------------- -Using ``@`` might confuse users expecting matrix multiplication. However, within -a Type Expression, Python intentionally reuses standard operators (like ``|`` +Using ``@`` might confuse users expecting matrix multiplication. Within +a type expression, Python intentionally reuses standard operators (like ``|`` and ``[]``) with typing-specific semantics. -Format.TYPE Coupling --------------------- +Format.TYPE and ``@`` in type hints +----------------------------------- -The coupling of ``Format.TYPE`` with Type Expression semantics is intentional +The coupling of ``Format.TYPE`` with type expression semantics is intentional and reinforces the boundary between typing evaluation and normal runtime -evaluation. For example, ``NoneType`` explicitly does not implement -``__matmul__`` to prevent masking runtime bugs. Because ``Format.TYPE`` uses -typing semantics, it can safely evaluate ``None @Metadata`` structurally without -altering global ``NoneType`` behavior. +evaluation. Frameworks relying on a custom ``@`` operator in annotations are +incompatible with the typing specification. These are considered non-typing +annotations. + +To clarify this boundary, ``Format.TYPE`` explicitly does not handle non-typing +annotations. They remain fully supported via ``Format.STRING`` or +``Format.VALUE``, and we do not break metaclasses that implement their own +``__matmul__``. + +This coupling also allows ``Format.TYPE`` to safely evaluate +``None @Metadata`` structurally without altering global ``NoneType`` behavior +(which explicitly does not implement ``__matmul__`` to prevent masking runtime +bugs). Future Work =========== -While this feature stands on its own, establishing ``@`` as a standard metadata -token opens the door to several speculative extensions. The following ideas -outline one possible direction the ecosystem could take. +Although this proposal stands on its own, establishing ``@`` as a metadata token +enables several future extensions. Native Symbol Decorators ------------------------ @@ -643,40 +555,36 @@ Developers frequently request framework-agnostic field decorators:: @Field(primary_key=True) id: int -Reserving ``@`` for metadata provides the grammatical foundation for Symbol -Decorators. Any future proposal must choose between two architectural models: +Any future proposal must choose between two architectural models: -- **The Descriptor Model (Active Runtime):** The decorator acts as a runtime - function returning a `descriptor `__, - actively intercepting attribute access (similar to standard Python function +- **The descriptor model:** The decorator acts as a runtime function returning a + `descriptor `__, actively + intercepting attribute access (similar to standard Python function decorators). -- **The Metadata Model (Passive Declaration):** The field configuration is - treated as passive Symbol Metadata, leaving the enclosing class to process it - during creation. +- **The metadata model:** The field configuration is treated as passive symbol + metadata, leaving the enclosing class to process it during creation. -This PEP anchors Python to the Metadata Model. The modern ecosystem is heavily -driven by type-directed libraries (Pydantic, SQLAlchemy, ``dataclasses``) that -inspect static definitions during class creation rather than relying on -standalone descriptors. +We lean towards the metadata model. The modern ecosystem is heavily driven by +type-directed libraries (Pydantic, SQLAlchemy, ``dataclasses``) that inspect +static definitions during class creation rather than relying on standalone +descriptors. -Because we are establishing the Metadata Model, ``@Field(...) id: int`` will -evaluate identically to ``id: int @Field(...)``. This alignment allows these -frameworks to inspect field configurations and continue working without -requiring extensive library changes. +``@Field(...) id: int`` would evaluate identically to ``id: int +@Field(...)``. This allows existing frameworks to inspect field configurations +and continue working without requiring any changes. .. note:: - This resolves the conceptual tension between value space and type space. - While standard decorators operate in value space (actively modifying runtime - objects), this syntax firmly establishes ``@`` in type space, safely - attaching Symbol Metadata without altering runtime execution. + This cleanly separates value space from type space. Decorators in value space + modify an object at runtime, while decorators in type space add metadata to a + type. Targeted Metadata ----------------- -Future extensions to :pep:`746` will support annotation targets. By intersecting -a base type with an explicit target constraint, type checkers will validate -*where* metadata is allowed to exist. This prevents misuse (e.g., placing a -``@Column`` on a function parameter rather than a class field): +Future extensions to :pep:`746` could support annotation targets. By +intersecting a base type with an explicit target constraint, type checkers will +validate *where* metadata is allowed to exist. This prevents misuse (e.g., +placing a ``@Column`` on a function parameter rather than a class field): *(Note: The following example assumes the ``&`` operator for intersection types has been added.)* @@ -697,24 +605,14 @@ richer typing ecosystem. Open Issues =========== -Deprecation Timeline --------------------- +**Deprecation Timeline:** As a private class, ``typing._AnnotatedAlias`` could +bypass the standard 5-year deprecation policy (:pep:`387`) for faster removal. +The exact schedule is open for discussion. -The removal of the ``typing._AnnotatedAlias`` compatibility layer is scheduled -for Python 3.21, following the standard 5-year deprecation policy (:pep:`387`). -However, given the widespread use of ``typing.Annotated``, the exact timeline -for removal (or whether the shim should be retained indefinitely to prevent -churn in legacy code) remains open for community discussion. - -``annotationlib.Format.TYPE`` Extraction ----------------------------------------- - -``Format.TYPE`` enables the structural evaluation of Type Expressions. While -required here to support ``type @annot``, it fundamentally improves the -evaluation of type expressions (such as correctly resolving ``|`` unions that -would otherwise be stringified by ``Format.FORWARDREF``). We invite discussion -on whether this broader semantic addition to ``annotationlib`` warrants -extraction into an independent PEP. +**annotationlib.Format.TYPE Extraction:** While not strictly required for +``type @annot``, ``Format.TYPE`` improves type expression evaluation generally +(e.g., correctly resolving ``|`` unions). We invite discussion on whether this +addition warrants an independent PEP. Acknowledgements ================