diff --git a/peps/pep-0835.rst b/peps/pep-0835.rst index 68cfa2b299d..edd6ce5f013 100644 --- a/peps/pep-0835.rst +++ b/peps/pep-0835.rst @@ -14,187 +14,182 @@ Post-History: `19-Apr-2026 `__. -Conceptual Consistency and Precedent -------------------------------------- +Adopting ``@`` for type metadata also follows the precedent of repurposing runtime +operators for static typing, as seen with ``[]`` for generics (:pep:`585`) and +``|`` for unions (:pep:`604`) [8]_. -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. +The postfix syntax mirrors features in other statically typed languages: -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. +- **C++:** Uses ``[[attribute]]`` for types (e.g., ``[[nodiscard]] int f();``). +- **Java / Kotlin:** Uses ``@Annotation`` (e.g., ``List<@NonNull String>`` or + ``val x: @NotNull String``). +- **OCaml:** Uses ``[@attribute]`` postfix syntax (e.g., + ``type t = int [@default 0]``). -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: +Terminology +=========== -.. code-block:: java +To clarify discussion around annotations and metadata, the following terms +apply: - public class Person { - @Column(length = 32) - private String name; - } +- **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 + via ``Annotated`` (:pep:`593`, :pep:`746`). +- **Non-typing annotation**: e.g., `@no_type_check + `__ + wrapping ``x: ``. Annotations that are not intended to be evaluated as + type expressions (e.g.: ignored by type checkers). +- **Symbol decorator**: Applying metadata directly to a variable or field + declaration, rather than to its underlying type. This distinction is + well-established in languages like Java: -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. + .. code-block:: java -Implementation and Performance -------------------------------- + class Application { + // Field Decorator (symbol decorator) + @Inject + private Service s; -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. + // Type Decorator (type metadata) + private @NonNull String name; + } -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. - -CPython prototype testing confirms that libraries like ``typer`` and -``pydantic`` work out of the box. +- **Symbol metadata** (or **field metadata**): Metadata attached to a + symbol. 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 ``@`` (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: +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:: -- ``int | str @ Metadata`` is equivalent to ``int | Annotated[str, Metadata]`` -- ``(int | str) @ Metadata`` is equivalent to ``Annotated[int | str, Metadata]`` + zip_code: int @Ge(10000) @Le(99999) | str @Len(5) -This matches the standard precedence of ``@`` and ``|`` in Python expressions. -The most common union pattern, ``Optional``, works naturally: +To attach metadata to the entire union, use parentheses:: -- ``int @ Field(gt=0) | None`` is equivalent to - ``Annotated[int, Field(gt=0)] | None`` + # Attaches only to 'str' + int | str @Metadata # equivalent to: int | Annotated[str, Metadata] -Flattening and Associativity ----------------------------- + # Attaches to the entire union + (int | str) @Metadata # equivalent to: Annotated[int | str, Metadata] -The ``@`` operator is left-associative. 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``. -This flattening also applies when the left-hand operand is an existing -``Annotated`` type, regardless of how it was constructed:: +Flattening Multiple Metadata +---------------------------- + +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. - Annotated[int, m1] @ m2 # AnnotatedType(int, m1, m2) — flattened +This same logic 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 - >>> type(int @ Field()) is type(Annotated[int, Field()]) + >>> type(int @Field()) is type(Annotated[int, Field()]) True >>> typing.Annotated is types.AnnotatedType True @@ -205,284 +200,487 @@ 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) - -``AnnotatedType`` objects support pickling via ``copyreg``, reconstructing -through ``AnnotatedType[origin, *metadata]``. + >>> int @Field(gt=0) + int @Field(gt=0) -``None`` on the left-hand side is accepted and uses ``None`` as the -origin: - -.. code-block:: pycon +Handling of ``None`` +-------------------- - >>> None @ Field() - None @ Field() +``NoneType`` explicitly avoids implementing ``__matmul__`` to prevent masking +runtime bugs. -Supported Left-Hand Operands ------------------------------ +For example, a developer might forget to check for ``None`` before matrix +multiplication: -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. +.. code-block:: + :class: bad -For all other left-hand operands, the operator returns ``NotImplemented``, -allowing normal ``__matmul__`` dispatch to proceed. + def matmul_arrays(a: np.array | None, b: np.array): + return a @ b # oops, forgot to check for None -Parsing and Grammar -=================== +If ``NoneType.__matmul__`` existed, this would silently return an +``AnnotatedType`` instead of raising a ``TypeError``. -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. +``annotationlib.Format.TYPE`` sidesteps this limitation in typing +expressions. It evaluates structurally, correctly parsing ``None @Metadata`` +into an ``AnnotatedType``. Outside of type expressions, use ``Annotated[None, +Metadata]``. -How to Teach This -================= +Supported Left-Hand Operands +----------------------------- -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)``." +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 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. +Applying ``@`` to a class evaluates as type metadata; applying it to an instance +performs arithmetic. -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. +For example, ``int @Field()`` produces an ``AnnotatedType``, while ``42 @ +something`` raises a ``TypeError`` (or delegates to ``__rmatmul__``). -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. +Likewise, ``ndarray @Field()`` produces an ``AnnotatedType``, even though +``ndarray`` instances define ``__matmul__`` for matrix multiplication. -Backwards Compatibility -======================= +Custom metaclasses can overload ``__matmul__`` provided ``@`` is avoided in type +expressions. Forward References and Deferred Evaluation ------------------------------------------- -Under :pep:`749`, annotations are lazily evaluated. The ``annotationlib`` -module provides several formats for retrieving annotations: +Under :pep:`749`'s lazy evaluation, existing ``annotationlib`` formats do not +preserve the structure of ``@`` expressions when names are unresolvable. +``Format.FORWARDREF`` stringifies unresolvable names:: -- ``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. + class Model: + ref: NotYetDefined @Field(gt=0) -For the ``@`` operator, ``Format.FORWARDREF`` is insufficient. Consider:: +This produces an opaque ``ForwardRef('"NotYetDefined" @Field(gt=0)')``. The +metadata is enclosed within the string, preventing libraries from easily +inspecting it. - class Model: - ref: "NotYetDefined" @ Field(gt=0) +``Format.TYPE`` assumes typing semantics and evaluates type expressions +structurally: -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. +* **Unresolvable Names:** Unresolvable names are wrapped in a ``ForwardRef`` + independently, leaving operators intact. +* **Union Types:** The ``|`` operator evaluates to ``types.UnionType``. +* **Annotated Types:** The ``@`` operator evaluates to ``types.AnnotatedType``. +* **Metadata Boundary:** Structural evaluation applies only to the type space. + Expressions within the metadata (the right-hand side of ``@`` or the + subsequent arguments of ``Annotated``) are evaluated as values. Unresolvable + expressions there produce a single opaque ``ForwardRef`` (identical to + ``Format.FORWARDREF``). -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:: +For example, the ``NotYetDefined @Field(gt=0)`` annotation evaluates into an +``AnnotatedType`` where the metadata remains immediately accessible:: 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``. +This also resolves the pre-existing limitation where ``"Foo" | int`` produced +``ForwardRef('Foo | int')`` under ``Format.FORWARDREF``. -Interaction with PEP 563 -^^^^^^^^^^^^^^^^^^^^^^^^^ +Rationale +========= -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``. +``Annotated`` forces type metadata to resemble a parameterized class. A postfix +operator provides identical semantics without nesting. -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. +Reusing the existing ``@`` operator leaves the Python parser unchanged. The +operator's new meaning is isolated to type evaluation, where ``T @M`` lowers to +``Annotated[T, M]``. Type checkers (Mypy, Pyright) also require no parser +changes, handling the syntax directly 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. -Operator Overloading --------------------- +Parentheses and Verbosity +------------------------- -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. - -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. - -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. - -typing.Annotated Migration ---------------------------- +Writing ``address: (str | None) @Field(...)`` adds verbosity due to the lack +of native symbol decorators. Frameworks co-opt ``Annotated`` to configure +fields, even though the developer's intent is to configure the ``address`` +field, not the ``str | None`` type. While the ``@`` shorthand cannot fully +eliminate this structural limitation, it significantly reduces the syntactic +weight of the workaround compared to ``Annotated[str | None, Field(...)]``. -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. +Why the @ Operator? +------------------- -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. +Selecting the correct operator for metadata involves balancing three +considerations: -Code that should be updated: +1. **Precedence:** Binding tighter than ``|`` (Union) and ``&`` (proposed + Intersection) ensures expressions like ``int @Field() | str`` parse correctly + unparenthesized. This excludes operators like ``|``, ``^``, and ``&``. +2. **Ecosystem Compatibility:** New keywords require ecosystem-wide parser + migrations. Because Python's type system is evolving into an arithmetic of + types (e.g., replacing ``Union`` with ``|``), reusing an operator extends + this pattern without unnecessary churn. +3. **Semantic Clarity:** The chosen syntax should avoid colliding with + established intuitions for primitive types. -- ``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`` +This leaves the set of overridable binary operators that bind tighter than +``&``: ``**``, ``*``, ``@``, ``/``, ``//``, ``%``, ``+``, ``-``, ``>>``, and +``<<``. -Backporting via typing_extensions ----------------------------------- +Standard arithmetic operators like ``+``, ``-``, ``/``, ``//``, ``*``, ``**``, +and ``%`` are misleading. Reading ``int + x`` or ``float / Field()`` strongly +implies mathematical evaluation, not metadata decoration. -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. +The remaining candidates are ``<<``, ``>>``, and ``@``. We chose ``@`` because +it already associates with metadata in Python via decorators. While libraries +like NumPy use it for matrix multiplication, it isn't as tied to arithmetic as +operators like ``+`` or ``/``. -Rejected Ideas -============== -Mandatory List Variant ----------------------- +Backwards Compatibility +======================= -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``. +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. -List-based syntax ------------------ +To ensure a smooth transition, this legacy 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 `Open Issues`_). -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. +Code that should be updated: -Scientific Computing Conflict ------------------------------ +- ``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:** 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. -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. +Security Implications +===================== -Since type annotations and arithmetic expressions occupy distinct syntactic -positions, this is a visual concern rather than a runtime conflict. +There are no direct security implications. -Divergence from Type Theory ---------------------------- +How to Teach This +================= -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`. +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)``." + +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 ``|``). + +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. + +**Visual Style:** Format the shorthand 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 excels 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 class Project(BaseModel): - name: str @ Field(title="Project Name") @ Len(1) - url: HttpUrl @ Field(description="The project homepage") - stars: int @ Field(ge=0) = 0 - -FastAPI Dependency Injection ----------------------------- + name: str @Field(title="Project Name") @Len(1) + url: HttpUrl @Field(description="The project homepage") + stars: int @Field(ge=0) = 0 -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 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 ---------------------------------- - -SQLModel relies heavily on ``Annotated`` to define column properties. The -shorthand syntax makes these definitions significantly cleaner:: +**SQLModel and Database Definitions:** SQLModel relies on ``Annotated`` to +define column properties. The shorthand syntax cleans up these definitions:: 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 and CrossHair use +``annotated-types`` to constrain test generation. 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 lacked consensus. The ``@`` symbol also extends cleanly to symbol +decorators if the language pursues that route later. + +Reliance on ``__rmatmul__`` +--------------------------- + +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]: + return Annotated[typ, self] + + class Le(Metadata[int]): + ... + +This approach is rejected for two reasons. First, relying on arbitrary +right-hand objects to implement ``__rmatmul__`` breaks the type expression +model, complicating the assignment of fixed static meanings to operators. This +is inconsistent with how ``|`` works and contradicts the type expression grammar +defined in the specification. Second, :pep:`593` explicitly permits any valid +Python object as metadata (e.g., strings, dicts). Implementing ``__matmul__`` +directly on the ``type`` metaclass avoids opt-in base classes and provides +consistent behavior. + +Parameter/Field Decorators +-------------------------- + +True parameter/field decorators were rejected for now. Modifying the core parser +for symbol-level decorators opens a complex design space; this PEP scopes the +``@`` operator to type expressions. + +Avoiding @ Due to Matrix Multiplication +--------------------------------------- + +We rejected the argument that ``@`` should be avoided because of its association +with matrix multiplication. Within a type expression, Python already reuses +standard operators (like ``|`` for unions and ``[]`` for generics) with +typing-specific semantics (see `Why the @ Operator?`_). + +Evaluating Type Expressions as Runtime Code +------------------------------------------- + +The design explicitly couples ``Format.TYPE`` to type expression semantics, +reinforcing the boundary between static typing and runtime evaluation: + +* **Non-Typing Annotations:** Frameworks using custom ``@`` operators in + annotations remain supported via ``Format.STRING`` and ``Format.VALUE``. They + are incompatible with ``Format.TYPE``, which enforces typing semantics. +* **Structural Evaluation:** Because ``Format.TYPE`` handles the ``@`` operator + structurally, it can evaluate constructs like ``None @Metadata`` even though + the global ``NoneType`` does not implement ``__matmul__``. + +Future Work +=========== + +Although this proposal stands on its own, establishing ``@`` for type metadata +enables several future extensions. + +Native Symbol Decorators +------------------------ + +Developers request framework-agnostic field decorators:: + + class User(BaseModel): + @Field(primary_key=True) + id: int + +Future proposals will likely need to navigate between two architectural models: + +- **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:** The field configuration is treated as passive symbol + metadata, leaving the enclosing class to process it during creation. + +This proposal aligns with the metadata model. Type-directed libraries typically +inspect static definitions during class creation rather than relying on +standalone descriptors. + +``@Field(...) id: int`` would evaluate identically to ``id: int +@Field(...)``. This allows existing frameworks to inspect field configurations +and continue working unmodified. Under this model, value-space decorators modify +objects at runtime, while type-space decorators attach metadata to a type. + +Targeted Metadata +----------------- + +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 mitigates 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] + +Establishing a native syntax for metadata provides a structural foundation for +future extensions like annotation targets. + +Open Issues +=========== + +**Deprecation Timeline:** As a private class, ``typing._AnnotatedAlias`` could +bypass the standard 5-year deprecation policy (:pep:`387`). Should we +fast-track its removal? + +**annotationlib.Format.TYPE Extraction:** ``Format.TYPE`` improves +type expression evaluation (e.g., properly resolving ``|`` unions). Does this +warrant a standalone 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] Major frameworks supporting ``Annotated`` include: + + * FastAPI support for ``Annotated``: + https://fastapi.tiangolo.com/tutorial/query-params-str-validations/ + + * Pydantic support for ``Annotated``: + https://docs.pydantic.dev/latest/concepts/types/#annotated-types + + * cattrs support for ``Annotated``: + https://cattrs.readthedocs.io/en/latest/validation.html#annotated + + * msgspec support for ``Annotated``: + https://jcristharif.com/msgspec/supported-types.html#annotated + + * 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 + + * Typer support for ``Annotated``: + https://typer.tiangolo.com/tutorial/parameter-types/annotated/ + + * Beartype support for ``Annotated`` (Validators): + https://beartype.readthedocs.io/en/latest/api_vale/ + +.. [2] Examples of users and framework authors citing ``Annotated`` verbosity: + + * *"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 + + * *"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 + + * 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 + +.. [3] *"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 + +.. [4] *"I'm not writing something stupidly verbose like: TensorBatchXChannels + = Annotated[...]"* — Patrick Kidger, author of TorchTyping: + https://github.com/beartype/beartype/discussions/96#discussioncomment-2245014 + +.. [5] *"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 + +.. [6] 2023 Python Discourse discussion proposing ``@`` as an alternative to + Annotated: + https://discuss.python.org/t/40751 + +.. [7] 2025 Python Discourse discussion converging on dedicated ``@`` syntax: + https://discuss.python.org/t/103699 + +.. [8] *"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.