diff --git a/peps/pep-0835.rst b/peps/pep-0835.rst index 68cfa2b299d..0b0b55aef2d 100644 --- a/peps/pep-0835.rst +++ b/peps/pep-0835.rst @@ -14,187 +14,190 @@ Post-History: `19-Apr-2026 `__. +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]_. -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. +Precedent in Other Languages +---------------------------- -CPython prototype testing confirms that libraries like ``typer`` and -``pydantic`` work out of the box. +The ``@`` 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: ``. 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 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) + @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 + 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: +If you intend to attach metadata to the entire union, you must 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] + + # Attaches to the entire union + (int | str) @Metadata # equivalent to: Annotated[int | str, Metadata] -Flattening and Associativity ----------------------------- -The ``@`` operator is left-associative. When multiple metadata items are -chained, the resulting ``Annotated`` object is flattened. +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 +---------------------------- -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``. +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 + 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 +208,470 @@ 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]``. - -``None`` on the left-hand side is accepted and uses ``None`` as the -origin: - -.. code-block:: pycon + >>> int @Field(gt=0) + int @Field(gt=0) - >>> None @ Field() - None @ Field() +Handling of ``None`` +-------------------- -Supported Left-Hand Operands ------------------------------ +``NoneType`` explicitly avoids implementing ``__matmul__`` to prevent masking +runtime bugs. -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. +For example, a developer might forget to validate ``None`` before executing +matrix multiplication on an array: -For all other left-hand operands, the operator returns ``NotImplemented``, -allowing normal ``__matmul__`` dispatch to proceed. +.. code-block:: + :class: bad -Parsing and Grammar -=================== + def matmul_arrays(a: np.array | None, b: np.array): + return a @ b # oops, forgot to check for None -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. +If ``NoneType.__matmul__`` existed, this would silently return an +``AnnotatedType`` instead of raising a ``TypeError``. -How to Teach This -================= +``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]``. -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 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. +Supported Left-Hand Operands +----------------------------- -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 ``@`` 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). -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. +``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. -Backwards Compatibility -======================= +Custom metaclasses can still overload ``__matmul__`` as long as ``@`` 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: - -- ``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 ``@``. ``Format.FORWARDREF`` stringifies 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``. - -Interaction with PEP 563 -^^^^^^^^^^^^^^^^^^^^^^^^^ +The metadata remains immediately accessible. This also resolves the pre-existing +limitation where ``"Foo" | int`` produced ``ForwardRef('Foo | int')`` under +``Format.FORWARDREF``. -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``. +Rationale +========= -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. +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. -Operator Overloading --------------------- - -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 ---------------------------- +Why the @ Operator? +------------------- -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. +Selecting the correct operator for metadata involves balancing three +considerations: + +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 ``&``. +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 intuitions for primitive types. + +This leaves the set of overridable binary operators that bind tighter than +``&``: ``**``, ``*``, ``@``, ``/``, ``//``, ``%``, ``+``, ``-``, ``>>``, and +``<<``. + +Standard arithmetic operators like ``+``, ``-``, ``/``, ``//``, ``*``, ``**``, +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). + +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 +------------------- -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. +While adding syntax increases structural complexity, the *cognitive* load +decreases. -Code that should be updated: +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. -- ``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`` +Backwards Compatibility +======================= -Backporting via typing_extensions ----------------------------------- +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. -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 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 `Open Issues`_). -Rejected Ideas -============== +Code that should be updated: -Mandatory List Variant ----------------------- +- ``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`` -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``. +**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. -List-based syntax ------------------ +Security Implications +===================== -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. +There are no direct security implications. -Scientific Computing Conflict ------------------------------ +How to Teach This +================= -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. +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)``." -Since type annotations and arithmetic expressions occupy distinct syntactic -positions, this is a visual concern rather than a runtime conflict. +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 ``|``). -Divergence from Type Theory ---------------------------- +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. -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`. +**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 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 heavily on +``Annotated`` to define column properties. The +shorthand syntax makes these definitions 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 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 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. + +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. + +Operator Overloading +-------------------- + +Using ``@`` might confuse users expecting matrix multiplication. Within +a type expression, Python intentionally reuses standard operators (like ``|`` +and ``[]``) with typing-specific semantics. + +Format.TYPE and ``@`` in type hints +----------------------------------- + +The coupling of ``Format.TYPE`` with type expression semantics is intentional +and reinforces the boundary between typing evaluation and normal runtime +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 +=========== + +Although this proposal stands on its own, establishing ``@`` as a metadata token +enables several future extensions. + +Native Symbol Decorators +------------------------ + +Developers frequently request framework-agnostic field decorators:: + + class User(BaseModel): + @Field(primary_key=True) + id: int + +Any future proposal must choose 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. + +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. + +``@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 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` 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.)* + +:: + + 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:** 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. + +**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 +================ + +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.