diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 01ae7534ba936..4eb6463e4bd43 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -2,7 +2,7 @@ Generics ======== This section explains how you can define your own generic classes that take -one or more type parameters, similar to built-in types such as ``list[X]``. +one or more type arguments, similar to built-in types such as ``list[T]``. User-defined generics are a moderately advanced feature and you can get far without ever using them -- feel free to skip this section and come back later. @@ -12,18 +12,48 @@ Defining generic classes ************************ The built-in collection classes are generic classes. Generic types -have one or more type parameters, which can be arbitrary types. For -example, ``dict[int, str]`` has the type parameters ``int`` and -``str``, and ``list[int]`` has a type parameter ``int``. +accept one or more type arguments within ``[...]``, which can be +arbitrary types. For example, the type ``dict[int, str]`` has the +type arguments ``int`` and ``str``, and ``list[int]`` has the type +argument ``int``. Programs can also define new generic classes. Here is a very simple -generic class that represents a stack: +generic class that represents a stack (using the syntax introduced in +Python 3.12): + +.. code-block:: python + + class Stack[T]: + def __init__(self) -> None: + # Create an empty list with items of type T + self.items: list[T] = [] + + def push(self, item: T) -> None: + self.items.append(item) + + def pop(self) -> T: + return self.items.pop() + + def empty(self) -> bool: + return not self.items + +There are two syntax variants for defining generic classes in Python. +Python 3.12 introduced a +`new dedicated syntax `_ +for defining generic classes (and also functions and type aliases, which +we will discuss later). The above example used the new syntax. Most examples are +given using both the new and the old (or legacy) syntax variants. +Unless mentioned otherwise, they work the same -- but the new syntax +is more readable and more convenient. + +Here is the same example using the old syntax (required for Python 3.11 +and earlier, but also supported on newer Python versions): .. code-block:: python from typing import TypeVar, Generic - T = TypeVar('T') + T = TypeVar('T') # Define type variable "T" class Stack(Generic[T]): def __init__(self) -> None: @@ -39,8 +69,16 @@ generic class that represents a stack: def empty(self) -> bool: return not self.items +.. note:: + + There are currently no plans to deprecate the legacy syntax. + You can freely mix code using the new and old syntax variants, + even within a single file (but *not* within a single class). + The ``Stack`` class can be used to represent a stack of any type: -``Stack[int]``, ``Stack[tuple[int, str]]``, etc. +``Stack[int]``, ``Stack[tuple[int, str]]``, etc. You can think of +``Stack[int]`` as referring to the definition of ``Stack`` above, +but with all instances of ``T`` replaced with ``int``. Using ``Stack`` is similar to built-in container types: @@ -50,19 +88,49 @@ Using ``Stack`` is similar to built-in container types: stack = Stack[int]() stack.push(2) stack.pop() - stack.push('x') # error: Argument 1 to "push" of "Stack" has incompatible type "str"; expected "int" -Construction of instances of generic types is type checked: + # error: Argument 1 to "push" of "Stack" has incompatible type "str"; expected "int" + stack.push('x') + + stack2: Stack[str] = Stack() + stack2.append('x') + +Construction of instances of generic types is type checked (Python 3.12 syntax): .. code-block:: python - class Box(Generic[T]): + class Box[T]: def __init__(self, content: T) -> None: self.content = content Box(1) # OK, inferred type is Box[int] Box[int](1) # Also OK - Box[int]('some string') # error: Argument 1 to "Box" has incompatible type "str"; expected "int" + + # error: Argument 1 to "Box" has incompatible type "str"; expected "int" + Box[int]('some string') + +Here is the definition of ``Box`` using the legacy syntax (Python 3.11 and earlier): + +.. code-block:: python + + from typing import TypeVar, Generic + + T = TypeVar('T') + + class Box(Generic[T]): + def __init__(self, content: T) -> None: + self.content = content + +.. note:: + + Before moving on, let's clarify some terminology. + The name ``T`` in ``class Stack[T]`` or ``class Stack(Generic[T])`` + declares a *type parameter* ``T`` (of class ``Stack``). + ``T`` is also called a *type variable*, especially in a type annotation, + such as in the signature of ``push`` above. + When the type ``Stack[...]`` is used in a type annotation, the type + within square brackets is called a *type argument*. + This is similar to the distinction between function parameters and arguments. .. _generic-subclasses: @@ -70,7 +138,37 @@ Defining subclasses of generic classes ************************************** User-defined generic classes and generic classes defined in :py:mod:`typing` -can be used as a base class for another class (generic or non-generic). For example: +can be used as a base class for another class (generic or non-generic). For +example (Python 3.12 syntax): + +.. code-block:: python + + from typing import Mapping, Iterator + + # This is a generic subclass of Mapping + class MyMapp[KT, VT](Mapping[KT, VT]): + def __getitem__(self, k: KT) -> VT: ... + def __iter__(self) -> Iterator[KT]: ... + def __len__(self) -> int: ... + + items: MyMap[str, int] # OK + + # This is a non-generic subclass of dict + class StrDict(dict[str, str]): + def __str__(self) -> str: + return f'StrDict({super().__str__()})' + + data: StrDict[int, int] # Error! StrDict is not generic + data2: StrDict # OK + + # This is a user-defined generic class + class Receiver[T]: + def accept(self, value: T) -> None: ... + + # This is a generic subclass of Receiver + class AdvancedReceiver[T](Receiver[T]): ... + +Here is the above example using the legacy syntax (Python 3.11 and earlier): .. code-block:: python @@ -92,7 +190,6 @@ can be used as a base class for another class (generic or non-generic). For exam def __str__(self) -> str: return f'StrDict({super().__str__()})' - data: StrDict[int, int] # Error! StrDict is not generic data2: StrDict # OK @@ -105,25 +202,27 @@ can be used as a base class for another class (generic or non-generic). For exam .. note:: - You have to add an explicit :py:class:`~typing.Mapping` base class + You have to add an explicit :py:class:`~collections.abc.Mapping` base class if you want mypy to consider a user-defined class as a mapping (and - :py:class:`~typing.Sequence` for sequences, etc.). This is because mypy doesn't use - *structural subtyping* for these ABCs, unlike simpler protocols - like :py:class:`~typing.Iterable`, which use :ref:`structural subtyping `. + :py:class:`~collections.abc.Sequence` for sequences, etc.). This is because + mypy doesn't use *structural subtyping* for these ABCs, unlike simpler protocols + like :py:class:`~collections.abc.Iterable`, which use + :ref:`structural subtyping `. -:py:class:`Generic ` can be omitted from bases if there are +When using the legacy syntax, :py:class:`Generic ` can be omitted +from bases if there are other base classes that include type variables, such as ``Mapping[KT, VT]`` in the above example. If you include ``Generic[...]`` in bases, then it should list all type variables present in other bases (or more, -if needed). The order of type variables is defined by the following +if needed). The order of type parameters is defined by the following rules: -* If ``Generic[...]`` is present, then the order of variables is +* If ``Generic[...]`` is present, then the order of parameters is always determined by their order in ``Generic[...]``. -* If there are no ``Generic[...]`` in bases, then all type variables +* If there are no ``Generic[...]`` in bases, then all type parameters are collected in the lexicographic order (i.e. by first appearance). -For example: +Example: .. code-block:: python @@ -142,12 +241,26 @@ For example: x: First[int, str] # Here T is bound to int, S is bound to str y: Second[int, str, Any] # Here T is Any, S is int, and U is str +When using the Python 3.12 syntax, all type parameters must always be +explicitly defined immediately after the class name within ``[...]``, and the +``Generic[...]`` base class is never used. + .. _generic-functions: Generic functions ***************** -Type variables can be used to define generic functions: +Functions can also be generic, i.e. they can have type parameters (Python 3.12 syntax): + +.. code-block:: python + + from collections.abc import Sequence + + # A generic function! + def first[T](seq: Sequence[T]) -> T: + return seq[0] + +Here is the same example using the legacy syntax (Python 3.11 and earlier): .. code-block:: python @@ -159,24 +272,25 @@ Type variables can be used to define generic functions: def first(seq: Sequence[T]) -> T: return seq[0] -As with generic classes, the type variable can be replaced with any -type. That means ``first`` can be used with any sequence type, and the -return type is derived from the sequence item type. For example: +As with generic classes, the type parameter ``T`` can be replaced with any +type. That means ``first`` can be passed an argument with any sequence type, +and the return type is derived from the sequence item type. Example: .. code-block:: python reveal_type(first([1, 2, 3])) # Revealed type is "builtins.int" - reveal_type(first(['a', 'b'])) # Revealed type is "builtins.str" + reveal_type(first(('a', 'b'))) # Revealed type is "builtins.str" -Note also that a single definition of a type variable (such as ``T`` -above) can be used in multiple generic functions or classes. In this -example we use the same type variable in two generic functions: +When using the legacy syntax, a single definition of a type variable +(such as ``T`` above) can be used in multiple generic functions or +classes. In this example we use the same type variable in two generic +functions to declarare type parameters: .. code-block:: python from typing import TypeVar, Sequence - T = TypeVar('T') # Declare type variable + T = TypeVar('T') # Define type variable def first(seq: Sequence[T]) -> T: return seq[0] @@ -184,20 +298,62 @@ example we use the same type variable in two generic functions: def last(seq: Sequence[T]) -> T: return seq[-1] +Since the Python 3.12 syntax is more concise, it doesn't need (or have) +an equivalent way of sharing type parameter definitions. + A variable cannot have a type variable in its type unless the type variable is bound in a containing generic class or function. +When calling a generic function, you can't explicitly pass the values of +type parameters as type arguments. The values of type parameters are always +inferred by mypy. This is not valid: + +.. code-block:: python + + first[int]([1, 2]) # Error: can't use [...] with generic function + +If you really need this, you can define a generic class with a ``__call__`` +method. + .. _generic-methods-and-generic-self: Generic methods and generic self ******************************** -You can also define generic methods — just use a type variable in the -method signature that is different from class type variables. In -particular, the ``self`` argument may also be generic, allowing a +You can also define generic methods. In +particular, the ``self`` parameter may also be generic, allowing a method to return the most precise type known at the point of access. In this way, for example, you can type check a chain of setter -methods: +methods (Python 3.12 syntax): + +.. code-block:: python + + class Shape: + def set_scale[T: Shape](self: T, scale: float) -> T: + self.scale = scale + return self + + class Circle(Shape): + def set_radius(self, r: float) -> 'Circle': + self.radius = r + return self + + class Square(Shape): + def set_width(self, w: float) -> 'Square': + self.width = w + return self + + circle: Circle = Circle().set_scale(0.5).set_radius(2.7) + square: Square = Square().set_scale(0.5).set_width(3.2) + +Without using generic ``self``, the last two lines could not be type +checked properly, since the return type of ``set_scale`` would be +``Shape``, which doesn't define ``set_radius`` or ``set_width``. + +When using the legacy syntax, just use a type variable in the +method signature that is different from class type parameters (if any +are defined). Here is the above example using the legacy +syntax (3.11 and earlier): .. code-block:: python @@ -223,24 +379,41 @@ methods: circle: Circle = Circle().set_scale(0.5).set_radius(2.7) square: Square = Square().set_scale(0.5).set_width(3.2) -Without using generic ``self``, the last two lines could not be type -checked properly, since the return type of ``set_scale`` would be -``Shape``, which doesn't define ``set_radius`` or ``set_width``. +Other uses include factory methods, such as copy and deserialization methods. +For class methods, you can also define generic ``cls``, using ``type[T]`` +or :py:class:`Type[T] ` (Python 3.12 syntax): + +.. code-block:: python + + class Friend: + other: "Friend | None" = None + + @classmethod + def make_pair[T: Friend](cls: type[T]) -> tuple[T, T]: + a, b = cls(), cls() + a.other = b + b.other = a + return a, b -Other uses are factory methods, such as copy and deserialization. -For class methods, you can also define generic ``cls``, using :py:class:`Type[T] `: + class SuperFriend(Friend): + pass + + a, b = SuperFriend.make_pair() + +Here is the same example using the legacy syntax (3.11 and earlier, though +3.9 and later can use lower-case ``type[T]``): .. code-block:: python - from typing import TypeVar, Type + from typing import TypeVar T = TypeVar('T', bound='Friend') class Friend: - other: "Friend" = None + other: "Friend | None" = None @classmethod - def make_pair(cls: Type[T]) -> tuple[T, T]: + def make_pair(cls: type[T]) -> tuple[T, T]: a, b = cls(), cls() a.other = b b.other = a @@ -262,16 +435,13 @@ possibly by making use of the ``Any`` type or a ``# type: ignore`` comment. Note that mypy lets you use generic self types in certain unsafe ways in order to support common idioms. For example, using a generic -self type in an argument type is accepted even though it's unsafe: +self type in an argument type is accepted even though it's unsafe (Python 3.12 +syntax): .. code-block:: python - from typing import TypeVar - - T = TypeVar("T") - class Base: - def compare(self: T, other: T) -> bool: + def compare[T: Base](self: T, other: T) -> bool: return False class Sub(Base): @@ -280,7 +450,7 @@ self type in an argument type is accepted even though it's unsafe: # This is unsafe (see below) but allowed because it's # a common pattern and rarely causes issues in practice. - def compare(self, other: Sub) -> bool: + def compare(self, other: 'Sub') -> bool: return self.x > other.x b: Base = Sub(42) @@ -293,12 +463,12 @@ Automatic self types using typing.Self Since the patterns described above are quite common, mypy supports a simpler syntax, introduced in :pep:`673`, to make them easier to use. -Instead of defining a type variable and using an explicit annotation +Instead of introducing a type parameter and using an explicit annotation for ``self``, you can import the special type ``typing.Self`` that is -automatically transformed into a type variable with the current class -as the upper bound, and you don't need an annotation for ``self`` (or -``cls`` in class methods). The example from the previous section can -be made simpler by using ``Self``: +automatically transformed into a method-level type parameter with the +current class as the upper bound, and you don't need an annotation for +``self`` (or ``cls`` in class methods). The example from the previous +section can be made simpler by using ``Self``: .. code-block:: python @@ -319,7 +489,7 @@ be made simpler by using ``Self``: a, b = SuperFriend.make_pair() -This is more compact than using explicit type variables. Also, you can +This is more compact than using explicit type parameters. Also, you can use ``Self`` in attribute annotations in addition to methods. .. note:: @@ -354,8 +524,8 @@ Let us illustrate this by few simple examples: class Triangle(Shape): ... class Square(Shape): ... -* Most immutable containers, such as :py:class:`~typing.Sequence` and - :py:class:`~typing.FrozenSet` are covariant. :py:data:`~typing.Union` is +* Most immutable container types, such as :py:class:`~collections.abc.Sequence` + and :py:class:`~frozenset` are covariant. :py:data:`~typing.Union` is also covariant in all variables: ``Union[Triangle, int]`` is a subtype of ``Union[Shape, int]``. @@ -367,7 +537,7 @@ Let us illustrate this by few simple examples: triangles: Sequence[Triangle] count_lines(triangles) # OK - def foo(triangle: Triangle, num: int): + def foo(triangle: Triangle, num: int) -> None: shape_or_number: Union[Shape, int] # a Triangle is a Shape, and a Shape is a valid Union[Shape, int] shape_or_number = triangle @@ -400,8 +570,8 @@ Let us illustrate this by few simple examples: triangle. If we give it a callable that can calculate the area of an arbitrary shape (not just triangles), everything still works. -* :py:class:`~typing.List` is an invariant generic type. Naively, one would think - that it is covariant, like :py:class:`~typing.Sequence` above, but consider this code: +* ``list`` is an invariant generic type. Naively, one would think + that it is covariant, like :py:class:`~collections.abc.Sequence` above, but consider this code: .. code-block:: python @@ -416,13 +586,48 @@ Let us illustrate this by few simple examples: add_one(my_circles) # This may appear safe, but... my_circles[-1].rotate() # ...this will fail, since my_circles[0] is now a Shape, not a Circle - Another example of invariant type is :py:class:`~typing.Dict`. Most mutable containers + Another example of invariant type is ``dict``. Most mutable containers are invariant. -By default, mypy assumes that all user-defined generics are invariant. -To declare a given generic class as covariant or contravariant use -type variables defined with special keyword arguments ``covariant`` or -``contravariant``. For example: +When using the Python 3.12 syntax for generics, mypy will automatically +infer the most flexible variance for each class type variable. Here +``Box`` will be inferred as covariant: + +.. code-block:: python + + class Box[T]: # this type is implilicitly covariant + def __init__(self, content: T) -> None: + self._content = content + + def get_content(self) -> T: + return self._content + + def look_into(box: Box[Shape]): ... + + my_box = Box(Square()) + look_into(my_box) # OK, but mypy would complain here for an invariant type + +Here the underscore prefix for ``_content`` is significant. Without an +underscore prefix, the class would be invariant, as the attribute would +be understood as a public, mutable attribute (a single underscore prefix +has no special significance for mypy in most other contexts). By declaring +the attribute as ``Final``, the class could still be made covariant: + +.. code-block:: python + + from typing import Final + + class Box[T]: # this type is implilicitly covariant + def __init__(self, content: T) -> None: + self.content: Final = content + + def get_content(self) -> T: + return self._content + +When using the legacy syntax, mypy assumes that all user-defined generics +are invariant by default. To declare a given generic class as covariant or +contravariant, use type variables defined with special keyword arguments +``covariant`` or ``contravariant``. For example (Python 3.11 or earlier): .. code-block:: python @@ -437,9 +642,9 @@ type variables defined with special keyword arguments ``covariant`` or def get_content(self) -> T_co: return self._content - def look_into(box: Box[Animal]): ... + def look_into(box: Box[Shape]): ... - my_box = Box(Cat()) + my_box = Box(Square()) look_into(my_box) # OK, but mypy would complain here for an invariant type .. _type-variable-upper-bound: @@ -449,24 +654,32 @@ Type variables with upper bounds A type variable can also be restricted to having values that are subtypes of a specific type. This type is called the upper bound of -the type variable, and is specified with the ``bound=...`` keyword -argument to :py:class:`~typing.TypeVar`. +the type variable, and it is specified using ``T: `` when using the +Python 3.12 syntax. In the definition of a generic function that uses +such a type variable ``T``, the type represented by ``T`` is assumed +to be a subtype of its upper bound, so the function can use methods +of the upper bound on values of type ``T`` (Python 3.12 syntax): .. code-block:: python - from typing import TypeVar, SupportsAbs + from typing import SupportsAbs - T = TypeVar('T', bound=SupportsAbs[float]) + def max_by_abs[T: SupportsAbs[float]](*xs: T) -> T: + # Okay, because T is a subtype of SupportsAbs[float]. + return max(xs, key=abs) -In the definition of a generic function that uses such a type variable -``T``, the type represented by ``T`` is assumed to be a subtype of -its upper bound, so the function can use methods of the upper bound on -values of type ``T``. +An upper bound can also be specified with the ``bound=...`` keyword +argument to :py:class:`~typing.TypeVar`. +Here is the example using the legacy syntax (Python 3.11 and earlier): .. code-block:: python - def largest_in_absolute_value(*xs: T) -> T: - return max(xs, key=abs) # Okay, because T is a subtype of SupportsAbs[float]. + from typing import TypeVar, SupportsAbs + + T = TypeVar('T', bound=SupportsAbs[float]) + + def max_by_abs(*xs: T) -> T: + return max(xs, key=abs) In a call to such a function, the type ``T`` must be replaced by a type that is a subtype of its upper bound. Continuing the example @@ -474,9 +687,9 @@ above: .. code-block:: python - largest_in_absolute_value(-3.5, 2) # Okay, has type float. - largest_in_absolute_value(5+6j, 7) # Okay, has type complex. - largest_in_absolute_value('a', 'b') # Error: 'str' is not a subtype of SupportsAbs[float]. + max_by_abs(-3.5, 2) # Okay, has type float. + max_by_abs(5+6j, 7) # Okay, has type complex. + max_by_abs('a', 'b') # Error: 'str' is not a subtype of SupportsAbs[float]. Type parameters of generic classes may also have upper bounds, which restrict the valid values for the type parameter in the same way. @@ -489,34 +702,34 @@ Type variables with value restriction By default, a type variable can be replaced with any type. However, sometimes it's useful to have a type variable that can only have some specific types as its value. A typical example is a type variable that can only have values -``str`` and ``bytes``: +``str`` and ``bytes``. This lets us define a function that can concatenate +two strings or bytes objects, but it can't be called with other argument +types (Python 3.12 syntax): .. code-block:: python - from typing import TypeVar + def concat[S: (str, bytes)](x: S, y: S) -> S: + return x + y - AnyStr = TypeVar('AnyStr', str, bytes) + concat('a', 'b') # Okay + concat(b'a', b'b') # Okay + concat(1, 2) # Error! -This is actually such a common type variable that :py:data:`~typing.AnyStr` is -defined in :py:mod:`typing` and we don't need to define it ourselves. -We can use :py:data:`~typing.AnyStr` to define a function that can concatenate -two strings or bytes objects, but it can't be called with other -argument types: +The same thing is also possibly using the legacy syntax (Python 3.11 or earlier): .. code-block:: python - from typing import AnyStr + from typing import TypeVar + + AnyStr = TypeVar('AnyStr', str, bytes) def concat(x: AnyStr, y: AnyStr) -> AnyStr: return x + y - concat('a', 'b') # Okay - concat(b'a', b'b') # Okay - concat(1, 2) # Error! - -Importantly, this is different from a union type, since combinations -of ``str`` and ``bytes`` are not accepted: +No matter which syntax you use, such a type variable is called a type variable +with a value restriction. Importantly, this is different from a union type, +since combinations of ``str`` and ``bytes`` are not accepted: .. code-block:: python @@ -524,11 +737,11 @@ of ``str`` and ``bytes`` are not accepted: In this case, this is exactly what we want, since it's not possible to concatenate a string and a bytes object! If we tried to use -``Union``, the type checker would complain about this possibility: +a union type, the type checker would complain about this possibility: .. code-block:: python - def union_concat(x: Union[str, bytes], y: Union[str, bytes]) -> Union[str, bytes]: + def union_concat(x: str | bytes, y: str | bytes) -> str | bytes: return x + y # Error: can't concatenate str and bytes Another interesting special case is calling ``concat()`` with a @@ -556,13 +769,19 @@ this is correct for ``concat``, since ``concat`` actually returns a >>> print(type(ss)) -You can also use a :py:class:`~typing.TypeVar` with a restricted set of possible -values when defining a generic class. For example, mypy uses the type -:py:class:`Pattern[AnyStr] ` for the return value of :py:func:`re.compile`, -since regular expressions can be based on a string or a bytes pattern. +You can also use type variables with a restricted set of possible +values when defining a generic class. For example, the type +:py:class:`Pattern[S] ` is used for the return +value of :py:func:`re.compile`, where ``S`` can be either ``str`` +or ``bytes``. Regular expressions can be based on a string or a +bytes pattern. + +A type variable may not have both a value restriction and an upper bound +(see :ref:`type-variable-upper-bound`). -A type variable may not have both a value restriction (see -:ref:`type-variable-upper-bound`) and an upper bound. +Note that you may come across :py:data:`~typing.AnyStr` imported from +:py:mod:`typing`. This feature is now deprecated, but it means the same +as our definition of ``AnyStr`` above. .. _declaring-decorators: @@ -571,11 +790,12 @@ Declaring decorators Decorators are typically functions that take a function as an argument and return another function. Describing this behaviour in terms of types can -be a little tricky; we'll show how you can use ``TypeVar`` and a special +be a little tricky; we'll show how you can use type variables and a special kind of type variable called a *parameter specification* to do so. Suppose we have the following decorator, not type annotated yet, -that preserves the original function's signature and merely prints the decorated function's name: +that preserves the original function's signature and merely prints the decorated +function's name: .. code-block:: python @@ -585,7 +805,7 @@ that preserves the original function's signature and merely prints the decorated return func(*args, **kwds) return wrapper -and we use it to decorate function ``add_forty_two``: +We can use it to decorate function ``add_forty_two``: .. code-block:: python @@ -611,7 +831,28 @@ Note that class decorators are handled differently than function decorators in mypy: decorating a class does not erase its type, even if the decorator has incomplete type annotations. -Here's how one could annotate the decorator: +Here's how one could annotate the decorator (Python 3.12 syntax): + +.. code-block:: python + + from typing import Any, Callable, cast + + # A decorator that preserves the signature. + def printing_decorator[F: Callable[..., Any]](func: F) -> F: + def wrapper(*args, **kwds): + print("Calling", func) + return func(*args, **kwds) + return cast(F, wrapper) + + @printing_decorator + def add_forty_two(value: int) -> int: + return value + 42 + + a = add_forty_two(3) + reveal_type(a) # Revealed type is "builtins.int" + add_forty_two('x') # Argument 1 to "add_forty_two" has incompatible type "str"; expected "int" + +Here is the example using the legacy syntax (Python 3.11 and earlier): .. code-block:: python @@ -636,15 +877,28 @@ Here's how one could annotate the decorator: This still has some shortcomings. First, we need to use the unsafe :py:func:`~typing.cast` to convince mypy that ``wrapper()`` has the same -signature as ``func``. See :ref:`casts `. +signature as ``func`` (see :ref:`casts `). Second, the ``wrapper()`` function is not tightly type checked, although wrapper functions are typically small enough that this is not a big problem. This is also the reason for the :py:func:`~typing.cast` call in the ``return`` statement in ``printing_decorator()``. -However, we can use a parameter specification (:py:class:`~typing.ParamSpec`), -for a more faithful type annotation: +However, we can use a parameter specification, introduced using ``**P``, +for a more faithful type annotation (Python 3.12 syntax): + +.. code-block:: python + + from typing import Callable + + def printing_decorator[**P, T](func: Callable[P, T]) -> Callable[P, T]: + def wrapper(*args: P.args, **kwds: P.kwargs) -> T: + print("Calling", func) + return func(*args, **kwds) + return wrapper + +The same is possible using the legacy syntax with :py:class:`~typing.ParamSpec` +(Python 3.11 and earlier): .. code-block:: python @@ -661,7 +915,27 @@ for a more faithful type annotation: return wrapper Parameter specifications also allow you to describe decorators that -alter the signature of the input function: +alter the signature of the input function (Python 3.12 syntax): + +.. code-block:: python + + from typing import Callable + + # We reuse 'P' in the return type, but replace 'T' with 'str' + def stringify[**P, T](func: Callable[P, T]) -> Callable[P, str]: + def wrapper(*args: P.args, **kwds: P.kwargs) -> str: + return str(func(*args, **kwds)) + return wrapper + + @stringify + def add_forty_two(value: int) -> int: + return value + 42 + + a = add_forty_two(3) + reveal_type(a) # Revealed type is "builtins.str" + add_forty_two('x') # error: Argument 1 to "add_forty_two" has incompatible type "str"; expected "int" + +Here is the above example using the legacy syntax (Python 3.11 and earlier): .. code-block:: python @@ -671,21 +945,31 @@ alter the signature of the input function: P = ParamSpec('P') T = TypeVar('T') - # We reuse 'P' in the return type, but replace 'T' with 'str' + # We reuse 'P' in the return type, but replace 'T' with 'str' def stringify(func: Callable[P, T]) -> Callable[P, str]: def wrapper(*args: P.args, **kwds: P.kwargs) -> str: return str(func(*args, **kwds)) return wrapper - @stringify +You can also insert an argument in a decorator (Python 3.12 syntax): + +.. code-block:: python + + from typing import Callable, Concatenate + + def printing_decorator[**P, T](func: Callable[P, T]) -> Callable[Concatenate[str, P], T]: + def wrapper(msg: str, /, *args: P.args, **kwds: P.kwargs) -> T: + print("Calling", func, "with", msg) + return func(*args, **kwds) + return wrapper + + @printing_decorator def add_forty_two(value: int) -> int: return value + 42 - a = add_forty_two(3) - reveal_type(a) # Revealed type is "builtins.str" - add_forty_two('x') # error: Argument 1 to "add_forty_two" has incompatible type "str"; expected "int" + a = add_forty_two('three', 3) -Or insert an argument: +Here is the same function using the legacy syntax (Python 3.11 and earlier): .. code-block:: python @@ -701,19 +985,31 @@ Or insert an argument: return func(*args, **kwds) return wrapper - @printing_decorator - def add_forty_two(value: int) -> int: - return value + 42 - - a = add_forty_two('three', 3) - .. _decorator-factories: Decorator factories ------------------- Functions that take arguments and return a decorator (also called second-order decorators), are -similarly supported via generics: +similarly supported via generics (Python 3.12 syntax): + +.. code-block:: python + + from typing import Any, Callable + + def route[F: Callable[..., Any]](url: str) -> Callable[[F], F]: + ... + + @route(url='/') + def index(request: Any) -> str: + return 'Hello world' + +Note that mypy infers that ``F`` is used to make the ``Callable`` return value +of ``route`` generic, instead of making ``route`` itself generic, since ``F`` is +only used in the return type. Python has no explicit syntax to mark that ``F`` +is only bound in the return value. + +Here is the example using the legacy syntax (Python 3.11 and earlier): .. code-block:: python @@ -729,23 +1025,21 @@ similarly supported via generics: return 'Hello world' Sometimes the same decorator supports both bare calls and calls with arguments. This can be -achieved by combining with :py:func:`@overload `: +achieved by combining with :py:func:`@overload ` (Python 3.12 syntax): .. code-block:: python - from typing import Any, Callable, Optional, TypeVar, overload - - F = TypeVar('F', bound=Callable[..., Any]) + from typing import Any, Callable, overload # Bare decorator usage @overload - def atomic(__func: F) -> F: ... + def atomic[F: Callable[..., Any]](func: F, /) -> F: ... # Decorator with arguments @overload - def atomic(*, savepoint: bool = True) -> Callable[[F], F]: ... + def atomic[F: Callable[..., Any]](*, savepoint: bool = True) -> Callable[[F], F]: ... # Implementation - def atomic(__func: Optional[Callable[..., Any]] = None, *, savepoint: bool = True): + def atomic(func: Callable[..., Any] | None = None, /, *, savepoint: bool = True): def decorator(func: Callable[..., Any]): ... # Code goes here if __func is not None: @@ -760,21 +1054,40 @@ achieved by combining with :py:func:`@overload `: @atomic(savepoint=False) def func2() -> None: ... +Here is the decorator from the example using the legacy syntax +(Python 3.11 and earlier): + +.. code-block:: python + + from typing import Any, Callable, Optional, TypeVar, overload + + F = TypeVar('F', bound=Callable[..., Any]) + + # Bare decorator usage + @overload + def atomic(func: F, /) -> F: ... + # Decorator with arguments + @overload + def atomic(*, savepoint: bool = True) -> Callable[[F], F]: ... + + # Implementation + def atomic(func: Optional[Callable[..., Any]] = None, /, *, savepoint: bool = True): + ... # Same as above + Generic protocols ***************** Mypy supports generic protocols (see also :ref:`protocol-types`). Several :ref:`predefined protocols ` are generic, such as -:py:class:`Iterable[T] `, and you can define additional generic protocols. Generic -protocols mostly follow the normal rules for generic classes. Example: +:py:class:`Iterable[T] `, and you can define additional +generic protocols. Generic protocols mostly follow the normal rules for +generic classes. Example (Python 3.12 syntax): .. code-block:: python - from typing import Protocol, TypeVar - - T = TypeVar('T') + from typing import Protocol - class Box(Protocol[T]): + class Box[T](Protocol): content: T def do_stuff(one: Box[str], other: Box[bytes]) -> None: @@ -794,15 +1107,29 @@ protocols mostly follow the normal rules for generic classes. Example: y: Box[int] = ... x = y # Error -- Box is invariant +Here is the definition of ``Box`` from the above example using the legacy +syntax (Python 3.11 and earlier): + +.. code-block:: python + + from typing import Protocol, TypeVar + + T = TypeVar('T') + + class Box(Protocol[T]): + content: T + Note that ``class ClassName(Protocol[T])`` is allowed as a shorthand for -``class ClassName(Protocol, Generic[T])``, as per :pep:`PEP 544: Generic protocols <544#generic-protocols>`, +``class ClassName(Protocol, Generic[T])`` when using the legacy syntax, +as per :pep:`PEP 544: Generic protocols <544#generic-protocols>`. +This form is only valid when using the legacy syntax. -The main difference between generic protocols and ordinary generic -classes is that mypy checks that the declared variances of generic -type variables in a protocol match how they are used in the protocol -definition. The protocol in this example is rejected, since the type -variable ``T`` is used covariantly as a return type, but the type -variable is invariant: +When using the legacy syntax, there is an important difference between +generic protocols and ordinary generic classes: mypy checks that the +declared variances of generic type variables in a protocol match how +they are used in the protocol definition. The protocol in this example +is rejected, since the type variable ``T`` is used covariantly as +a return type, but the type variable is invariant: .. code-block:: python @@ -830,13 +1157,11 @@ This example correctly uses a covariant type variable: See :ref:`variance-of-generics` for more about variance. -Generic protocols can also be recursive. Example: +Generic protocols can also be recursive. Example (Python 3.12 synta): .. code-block:: python - T = TypeVar('T') - - class Linked(Protocol[T]): + class Linked[T](Protocol): val: T def next(self) -> 'Linked[T]': ... @@ -849,17 +1174,63 @@ Generic protocols can also be recursive. Example: result = last(L()) reveal_type(result) # Revealed type is "builtins.int" +Here is the definition of ``Linked`` using the legacy syntax +(Python 3.11 and earlier): + +.. code-block:: python + + from typing import TypeVar + + T = TypeVar('T') + + class Linked(Protocol[T]): + val: T + def next(self) -> 'Linked[T]': ... + .. _generic-type-aliases: Generic type aliases ******************** -Type aliases can be generic. In this case they can be used in two ways: -Subscripted aliases are equivalent to original types with substituted type -variables, so the number of type arguments must match the number of free type variables -in the generic type alias. Unsubscripted aliases are treated as original types with free -variables replaced with ``Any``. Examples (following :pep:`PEP 484: Type aliases -<484#type-aliases>`): +Type aliases can be generic. In this case they can be used in two ways. +First, subscripted aliases are equivalent to original types with substituted type +variables. Second, unsubscripted aliases are treated as original types with type +parameters replaced with ``Any``. + +The ``type`` statement introduced in Python 3.12 is used to define generic +type aliases (it also supports non-generic type aliases): + +.. code-block:: python + + from typing import Iterable, Callable + + type TInt[S] = tuple[int, S] + type UInt[S] = S | int + type CBack[S] = Callable[..., S] + + def response(query: str) -> UInt[str]: # Same as str | int + ... + def activate[S](cb: CBack[S]) -> S: # Same as Callable[..., S] + ... + table_entry: TInt # Same as tuple[int, Any] + + type Vec[T: (int, float, complex)] = Iterable[tuple[T, T]] + + def inproduct[T: (int, float, complex)](v: Vec[T]) -> T: + return sum(x*y for x, y in v) + + def dilate[T: (int, float, complex)](v: Vec[T], scale: T) -> Vec[T]: + return ((x * scale, y * scale) for x, y in v) + + v1: Vec[int] = [] # Same as Iterable[tuple[int, int]] + v2: Vec = [] # Same as Iterable[tuple[Any, Any]] + v3: Vec[int, int] = [] # Error: Invalid alias, too many type arguments! + +There is also a legacy syntax that relies on ``TypeVar``. +Here the number of type arguments must match the number of free type variables +in the generic type alias definition. A type variables is free if it's not +a type parameter of a surrounding class or function. Example (following +:pep:`PEP 484: Type aliases <484#type-aliases>`, Python 3.11 and earlier): .. code-block:: python @@ -867,7 +1238,7 @@ variables replaced with ``Any``. Examples (following :pep:`PEP 484: Type aliases S = TypeVar('S') - TInt = tuple[int, S] + TInt = tuple[int, S] # 1 type parameter, since only S is free UInt = Union[S, int] CBack = Callable[..., S] @@ -894,7 +1265,36 @@ variables replaced with ``Any``. Examples (following :pep:`PEP 484: Type aliases Type aliases can be imported from modules just like other names. An alias can also target another alias, although building complex chains of aliases is not recommended -- this impedes code readability, thus -defeating the purpose of using aliases. Example: +defeating the purpose of using aliases. Example (Python 3.12 syntax): + +.. code-block:: python + + from example1 import AliasType + from example2 import Vec + + # AliasType and Vec are type aliases (Vec as defined above) + + def fun() -> AliasType: + ... + + type OIntVec = Vec[int] | None + +Type aliases defined using the ``type`` statement are not valid as +base classes, and they can't be used to construct instances: + +.. code-block:: python + + from example1 import AliasType + from example2 import Vec + + # AliasType and Vec are type aliases (Vec as defined above) + + class NewVec[T](Vec[T]): # Error: not valid as base class + ... + + x = AliasType() # Error: can't be used to create instances + +Here are examples using the legacy syntax (Python 3.11 and earlier): .. code-block:: python @@ -907,18 +1307,49 @@ defeating the purpose of using aliases. Example: def fun() -> AliasType: ... + OIntVec = Optional[Vec[int]] + T = TypeVar('T') + # Old-style type aliases can be used as base classes and you can + # construct instances using them + class NewVec(Vec[T]): ... + x = AliasType() + for i, j in NewVec[int](): ... - OIntVec = Optional[Vec[int]] +Using type variable bounds or value restriction in generic aliases has +the same effect as in generic classes and functions. + + +Differences between the new and old syntax +****************************************** -Using type variable bounds or values in generic aliases has the same effect -as in generic classes/functions. +There are a few notable differences between the new (Python 3.12 and later) +and the old syntax for generic classes, functions and type aliases, beyond +the obvious syntactic differences: + + * Type variables defined using the old syntax create definitions at runtime + in the surrounding namespace, whereas the type variables defined using the + new syntax are only defined within the class, function or type variable + that uses them. + * Type variable definitions can be shared when using the old syntax, but + the new syntax doesn't support this. + * When using the new syntax, the variance of class type variables is always + inferred. + * Type aliases defined using the new syntax can contain forward references + and recursive references without using string literal escaping. The + same is true for the bounds and constraints of type variables. + * The new syntax lets you define a generic alias where the definition doesn't + contain a reference to a type parameter. This is occasionally useful, at + least when conditionally defining type aliases. + * Type aliases defined using the new syntax can't be used as base classes + and can't be used to construct instances, unlike aliases defined using the + old syntax. Generic class internals @@ -926,7 +1357,20 @@ Generic class internals You may wonder what happens at runtime when you index a generic class. Indexing returns a *generic alias* to the original class that returns instances -of the original class on instantiation: +of the original class on instantiation (Python 3.12 syntax): + +.. code-block:: python + + >>> class Stack[T]: ... + >>> Stack + __main__.Stack + >>> Stack[int] + __main__.Stack[int] + >>> instance = Stack[int]() + >>> instance.__class__ + __main__.Stack + +Here is the same example using the legacy syntax (Python 3.11 and earlier): .. code-block:: python @@ -945,10 +1389,17 @@ Generic aliases can be instantiated or subclassed, similar to real classes, but the above examples illustrate that type variables are erased at runtime. Generic ``Stack`` instances are just ordinary Python objects, and they have no extra runtime overhead or magic due -to being generic, other than a metaclass that overloads the indexing -operator. +to being generic, other than the ``Generic`` base class that overloads +the indexing operator using ``__class_getitem__``. ``typing.Generic`` +is included as an implicit base class even when using the new syntax: + +.. code-block:: python + + >>> class Stack[T]: ... + >>> Stack.mro() + [, , ] -Note that in Python 3.8 and lower, the built-in types +Note that in Python 3.8 and earlier, the built-in types :py:class:`list`, :py:class:`dict` and others do not support indexing. This is why we have the aliases :py:class:`~typing.List`, :py:class:`~typing.Dict` and so on in the :py:mod:`typing` @@ -959,16 +1410,18 @@ class in more recent versions of Python: .. code-block:: python >>> # Only relevant for Python 3.8 and below - >>> # For Python 3.9 onwards, prefer `list[int]` syntax + >>> # If using Python 3.9 or newer, prefer the 'list[int]' syntax >>> from typing import List >>> List[int] typing.List[int] Note that the generic aliases in ``typing`` don't support constructing -instances: +instances, unlike the corresponding built-in classes: .. code-block:: python + >>> list[int]() + [] >>> from typing import List >>> List[int]() Traceback (most recent call last):