From d3262103cb668fc59950047354d1d82169e98843 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 23 Sep 2024 14:03:48 +0100 Subject: [PATCH 01/13] Discuss 3.12 generics syntax in a few sections --- docs/source/generics.rst | 102 +++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 10 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 01ae7534ba93..d5ab4c91926a 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -16,8 +16,34 @@ 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``. +Python 3.12 introduced a new syntax for defining generic classes +and functions. Most examples are given using both the new and +old syntax. Unless explicitly mentioned otherwise, they behave +the same -- but the new syntax is more readable and convenient +to use. + Programs can also define new generic classes. Here is a very simple -generic class that represents a stack: +generic class that represents a stack (Python 3.12 syntax): + +.. 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 + + +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 @@ -52,11 +78,11 @@ Using ``Stack`` is similar to built-in container types: 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: +Construction of instances of generic types is type checked (Python 3.12): .. code-block:: python - class Box(Generic[T]): + class Box[T]: def __init__(self, content: T) -> None: self.content = content @@ -64,13 +90,52 @@ Construction of instances of generic types is type checked: Box[int](1) # Also OK Box[int]('some string') # error: Argument 1 to "Box" has incompatible type "str"; expected "int" +Here is the class definition using the legacy syntax (Python 3.11 and earlier): + +.. code-block:: python + + class Box(Generic[T]): + def __init__(self, content: T) -> None: + self.content = content + + .. _generic-subclasses: 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 +157,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 @@ -111,7 +175,8 @@ can be used as a base class for another class (generic or non-generic). For exam *structural subtyping* for these ABCs, unlike simpler protocols like :py:class:`~typing.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, @@ -142,12 +207,25 @@ 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, and the ``Generic[...]`` base class is never used. + .. _generic-functions: Generic functions ***************** -Type variables can be used to define generic functions: +Type variables can be used to define generic functions (Python 3.12): + +.. code-block:: python + + from typing 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 @@ -168,9 +246,10 @@ return type is derived from the sequence item type. For example: reveal_type(first([1, 2, 3])) # Revealed type is "builtins.int" 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: .. code-block:: python @@ -184,6 +263,9 @@ 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 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. From 9878381619418f68b115c59bc324ca2db48da616 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 23 Sep 2024 15:01:20 +0100 Subject: [PATCH 02/13] Update more documentation to cover 3.12 syntax --- docs/source/generics.rst | 137 +++++++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 27 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index d5ab4c91926a..9ca1afd7a888 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -279,7 +279,29 @@ method signature that is different from class type variables. In particular, the ``self`` argument 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](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) + +Here is the same example using the legacy syntax (3.11 and earlier): .. code-block:: python @@ -310,7 +332,28 @@ checked properly, since the return type of ``set_scale`` would be ``Shape``, which doesn't define ``set_radius`` or ``set_width``. Other uses are factory methods, such as copy and deserialization. -For class methods, you can also define generic ``cls``, using :py:class:`Type[T] `: +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 + + @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 + + 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 @@ -344,16 +387,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](self: T, other: T) -> bool: return False class Sub(Base): @@ -362,7 +402,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) @@ -375,7 +415,7 @@ 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 variable 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 @@ -449,7 +489,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 @@ -482,7 +522,7 @@ 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 +* ``list`` is an invariant generic type. Naively, one would think that it is covariant, like :py:class:`~typing.Sequence` above, but consider this code: .. code-block:: python @@ -498,13 +538,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 be 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[Animal]): ... + + my_box = Box(Cat()) + 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 in 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 @@ -531,24 +606,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 From 4829777901880754809a34f66d3b99bf2ed6aca3 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 23 Sep 2024 15:53:14 +0100 Subject: [PATCH 03/13] Update TypeVar with value restriction --- docs/source/generics.rst | 57 ++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 9ca1afd7a888..619adbacb885 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -654,33 +654,33 @@ 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 +No matter which syntax you use, this 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 @@ -689,11 +689,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 @@ -721,13 +721,26 @@ 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`). + +Note that if you use the legacy syntax, :py:data:`~typing.AnyStr` as we defined +it above is predefined in :py:mod:`typing`, and it isn't necessary to define it: + +.. code-block:: python + + from typing import AnyStr + + def concat(x: AnyStr, y: AnyStr) -> AnyStr: + return x + y -A type variable may not have both a value restriction (see -:ref:`type-variable-upper-bound`) and an upper bound. .. _declaring-decorators: From 706c4f79c64fe053e98ea4c799c60c89e41d53af Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 23 Sep 2024 16:04:05 +0100 Subject: [PATCH 04/13] Update section on decorators --- docs/source/generics.rst | 97 ++++++++++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 19 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 619adbacb885..1908b0ad135f 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -749,11 +749,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 @@ -763,7 +764,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 @@ -789,7 +790,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 @@ -814,15 +836,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 @@ -839,7 +874,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 @@ -855,15 +910,25 @@ alter the signature of the input function: 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 @@ -879,12 +944,6 @@ 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 From 07d50b4ee4a047c1724997f3e20a0a4da98385dd Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 23 Sep 2024 16:43:04 +0100 Subject: [PATCH 05/13] Update more docs --- docs/source/generics.rst | 108 ++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 24 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 1908b0ad135f..74352559b1d0 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -950,7 +950,25 @@ 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 @@ -966,20 +984,18 @@ 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): .. code-block:: python - from typing import Any, Callable, Optional, TypeVar, overload - - F = TypeVar('F', bound=Callable[..., Any]) + from typing import Any, Callable, Optional, 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): @@ -997,21 +1013,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: @@ -1031,15 +1066,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 @@ -1067,13 +1116,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]': ... @@ -1086,6 +1133,19 @@ 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 From 98a26e96cc93b11b12c9158a52187deed53dd2db Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 23 Sep 2024 17:52:35 +0100 Subject: [PATCH 06/13] More updates --- docs/source/generics.rst | 161 ++++++++++++++++++++++++++++++++++----- 1 file changed, 140 insertions(+), 21 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 74352559b1d0..3a6c1c8c1ded 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -16,14 +16,23 @@ 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``. -Python 3.12 introduced a new syntax for defining generic classes -and functions. Most examples are given using both the new and -old syntax. Unless explicitly mentioned otherwise, they behave -the same -- but the new syntax is more readable and convenient -to use. +Programs can also define new generic classes. 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). Most examples are given using both the new and +the old (or legacy) syntax variants. Unless explicitly mentioned otherwise, +they behave identically -- but the new syntax is more readable and +convenient to use. -Programs can also define new generic classes. Here is a very simple -generic class that represents a stack (Python 3.12 syntax): +.. note:: + + There are no plans to deprecate the legacy syntax in the future. + You can freely mix code using the new and old syntax variants, + even within a single file. + +Here is a very simple generic class that represents a stack +(Python 3.12 syntax): .. code-block:: python @@ -41,7 +50,6 @@ generic class that represents a stack (Python 3.12 syntax): def empty(self) -> bool: return not self.items - Here is the same example using the old syntax (required for Python 3.11 and earlier, but also supported on newer Python versions): @@ -78,7 +86,7 @@ Using ``Stack`` is similar to built-in container types: 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 (Python 3.12): +Construction of instances of generic types is type checked (Python 3.12 syntax): .. code-block:: python @@ -98,7 +106,6 @@ Here is the class definition using the legacy syntax (Python 3.11 and earlier): def __init__(self, content: T) -> None: self.content = content - .. _generic-subclasses: Defining subclasses of generic classes @@ -1155,8 +1162,39 @@ 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>`): +variables 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`` (following +:pep:`PEP 484: Type aliases <484#type-aliases>`, Python 3.11 and earlier): .. code-block:: python @@ -1191,7 +1229,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 @@ -1204,26 +1271,69 @@ 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 values in generic aliases has the same effect as in generic classes/functions. +Differences between the new and old syntax +****************************************** + +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 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 *********************** 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 @@ -1242,10 +1352,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` @@ -1256,16 +1373,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): From 18d116c86569f53dac6b1c3fac63bd26f8679889 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 23 Sep 2024 18:05:16 +0100 Subject: [PATCH 07/13] Minor updates --- docs/source/generics.rst | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 3a6c1c8c1ded..4daca149f5c9 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -84,7 +84,9 @@ 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" + + # error: Argument 1 to "push" of "Stack" has incompatible type "str"; expected "int" + stack.push('x') Construction of instances of generic types is type checked (Python 3.12 syntax): @@ -96,12 +98,18 @@ Construction of instances of generic types is type checked (Python 3.12 syntax): 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 class definition 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 @@ -222,7 +230,7 @@ explicitly defined, and the ``Generic[...]`` base class is never used. Generic functions ***************** -Type variables can be used to define generic functions (Python 3.12): +Type variables can be used to define generic functions (Python 3.12 syntax): .. code-block:: python @@ -270,8 +278,8 @@ functions: def last(seq: Sequence[T]) -> T: return seq[-1] -Since the Python 3.12 syntax is more concise, it doesn't have an -equivalent way of sharing type parameter definitions. +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. @@ -646,9 +654,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. From 3a7ef38b306f84667c78c67e08026b287e5eafa9 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 24 Sep 2024 16:14:39 +0100 Subject: [PATCH 08/13] Fixes and clarifications --- docs/source/generics.rst | 140 +++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 56 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 4daca149f5c9..0d991659464d 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,27 +12,14 @@ 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. 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). Most examples are given using both the new and -the old (or legacy) syntax variants. Unless explicitly mentioned otherwise, -they behave identically -- but the new syntax is more readable and -convenient to use. - -.. note:: - - There are no plans to deprecate the legacy syntax in the future. - You can freely mix code using the new and old syntax variants, - even within a single file. - -Here is a very simple generic class that represents a stack -(Python 3.12 syntax): +Programs can also define new generic classes. Here is a very simple +generic class that represents a stack (using the syntax introduced in +Python 3.12): .. code-block:: python @@ -50,6 +37,14 @@ Here is a very simple generic class that represents a stack 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): @@ -57,7 +52,7 @@ and earlier, but also supported on newer Python versions): from typing import TypeVar, Generic - T = TypeVar('T') + T = TypeVar('T') # Define type variable "T" class Stack(Generic[T]): def __init__(self) -> None: @@ -73,8 +68,16 @@ and earlier, but also supported on newer Python versions): 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. + 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: @@ -88,6 +91,9 @@ Using ``Stack`` is similar to built-in container types: # 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 @@ -102,7 +108,7 @@ Construction of instances of generic types is type checked (Python 3.12 syntax): # error: Argument 1 to "Box" has incompatible type "str"; expected "int" Box[int]('some string') -Here is the class definition using the legacy syntax (Python 3.11 and earlier): +Here is the definition of ``Box`` using the legacy syntax (Python 3.11 and earlier): .. code-block:: python @@ -114,6 +120,17 @@ Here is the class definition using the legacy syntax (Python 3.11 and earlier): 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: Defining subclasses of generic classes @@ -195,15 +212,15 @@ 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 @@ -223,14 +240,15 @@ For example: 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, and the ``Generic[...]`` base class is never used. +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 (Python 3.12 syntax): +Functions can also be generic, i.e. they can have type parameters (Python 3.12 syntax): .. code-block:: python @@ -252,25 +270,25 @@ Here is the same example using the legacy syntax (Python 3.11 and earlier): 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" 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: +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] @@ -284,13 +302,20 @@ 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 + .. _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 +You can also define generic methods. In particular, the ``self`` argument 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 @@ -299,7 +324,7 @@ methods (Python 3.12 syntax): .. code-block:: python class Shape: - def set_scale[T](self: T, scale: float) -> T: + def set_scale[T: Shape](self: T, scale: float) -> T: self.scale = scale return self @@ -316,7 +341,14 @@ methods (Python 3.12 syntax): circle: Circle = Circle().set_scale(0.5).set_radius(2.7) square: Square = Square().set_scale(0.5).set_width(3.2) -Here is the same example using the legacy syntax (3.11 and earlier): +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 abaove example using the legacy +syntax (3.11 and earlier): .. code-block:: python @@ -342,18 +374,14 @@ Here is the same example using the legacy syntax (3.11 and earlier): 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 are factory methods, such as copy and deserialization. +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 + other: "Friend | None" = None @classmethod def make_pair[T: Friend](cls: type[T]) -> tuple[T, T]: @@ -372,15 +400,15 @@ Here is the same example using the legacy syntax (3.11 and earlier, though .. 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 @@ -408,7 +436,7 @@ syntax): .. code-block:: python class Base: - def compare[T](self: T, other: T) -> bool: + def compare[T: Base](self: T, other: T) -> bool: return False class Sub(Base): @@ -430,12 +458,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 introducing 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 @@ -456,7 +484,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:: From 6b870b3fbc45c8e6b4613678494f1d427316703f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 24 Sep 2024 16:48:25 +0100 Subject: [PATCH 09/13] Minor tweaks --- docs/source/generics.rst | 41 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 0d991659464d..5dad13fa2940 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -597,16 +597,16 @@ infer the most flexible variance for each class type variable. Here def get_content(self) -> T: 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 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 in other contexts). By declaring the attribute -as ``Final``, the class could still be made covariant: +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 @@ -637,9 +637,9 @@ contravariant, use type variables defined with special keyword arguments 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: @@ -722,9 +722,9 @@ The same thing is also possibly using the legacy syntax (Python 3.11 or earlier) def concat(x: AnyStr, y: AnyStr) -> AnyStr: return x + y -No matter which syntax you use, this 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: +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 @@ -774,16 +774,9 @@ bytes pattern. A type variable may not have both a value restriction and an upper bound (see :ref:`type-variable-upper-bound`). -Note that if you use the legacy syntax, :py:data:`~typing.AnyStr` as we defined -it above is predefined in :py:mod:`typing`, and it isn't necessary to define it: - -.. code-block:: python - - from typing import AnyStr - - def concat(x: AnyStr, y: AnyStr) -> AnyStr: - return x + y - +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: @@ -923,7 +916,7 @@ alter the signature of the input function (Python 3.12 syntax): from typing import Callable - # 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[**P, T](func: Callable[P, T]) -> Callable[P, str]: def wrapper(*args: P.args, **kwds: P.kwargs) -> str: return str(func(*args, **kwds)) @@ -947,7 +940,7 @@ Here is the above example using the legacy syntax (Python 3.11 and earlier): 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)) @@ -1027,11 +1020,11 @@ Here is the example using the legacy syntax (Python 3.11 and earlier): 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 ` (Python 3.12): +achieved by combining with :py:func:`@overload ` (Python 3.12 syntax): .. code-block:: python - from typing import Any, Callable, Optional, overload + from typing import Any, Callable, overload # Bare decorator usage @overload @@ -1041,7 +1034,7 @@ achieved by combining with :py:func:`@overload ` (Python 3.12): 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: From 35adf86a50f7f4fa655ed2c591749b3452bb419c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 24 Sep 2024 16:55:18 +0100 Subject: [PATCH 10/13] Minor tweaks --- docs/source/generics.rst | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 5dad13fa2940..73648ef186b8 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -1187,11 +1187,10 @@ Here is the definition of ``Linked`` using the legacy syntax 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``. +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): @@ -1222,7 +1221,10 @@ type aliases (it also supports non-generic type aliases): 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`` (following +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 @@ -1231,7 +1233,7 @@ There is also a legacy syntax that relies on ``TypeVar`` (following 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] @@ -1315,8 +1317,8 @@ Here are examples using the legacy syntax (Python 3.11 and earlier): for i, j in NewVec[int](): ... -Using type variable bounds or values in generic aliases has the same effect -as in generic classes/functions. +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 From 5992dd919de0ccdda9865ab1d6b707a00d99d093 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 25 Sep 2024 11:19:14 +0100 Subject: [PATCH 11/13] Apply suggestions from code review Co-authored-by: Jelle Zijlstra Co-authored-by: Brian Schubert --- docs/source/generics.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 73648ef186b8..93e9ab04b674 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -316,7 +316,7 @@ Generic methods and generic self ******************************** You can also define generic methods. In -particular, the ``self`` argument may also be generic, allowing a +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 (Python 3.12 syntax): @@ -347,7 +347,7 @@ checked properly, since the return type of ``set_scale`` would be 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 abaove example using the legacy +are defined). Here is the above example using the legacy syntax (3.11 and earlier): .. code-block:: python @@ -584,7 +584,7 @@ Let us illustrate this by few simple examples: Another example of invariant type is ``dict``. Most mutable containers are invariant. -When using the Python 3.12 syntax for generics, mypy will be automatically +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: @@ -1028,13 +1028,13 @@ achieved by combining with :py:func:`@overload ` (Python 3.12 s # Bare decorator usage @overload - def atomic[F: Callable[..., Any]](__func: F) -> F: ... + def atomic[F: Callable[..., Any]](func: F, /) -> F: ... # Decorator with arguments @overload def atomic[F: Callable[..., Any]](*, savepoint: bool = True) -> Callable[[F], F]: ... # Implementation - def atomic(__func: Callable[..., Any] | None = 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: @@ -1060,13 +1060,13 @@ Here is the decorator from the example using the legacy syntax # Bare decorator usage @overload - def atomic(__func: F) -> F: ... + 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): + def atomic(func: Optional[Callable[..., Any]] = None, /, *, savepoint: bool = True): ... # Same as above Generic protocols @@ -1337,7 +1337,8 @@ the obvious syntactic differences: * 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. + 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. From 7e3eb7d19ff6b64cb2928c35f1d91d542b8d3426 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 25 Sep 2024 11:52:41 +0100 Subject: [PATCH 12/13] Apply suggestions from review --- docs/source/generics.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 93e9ab04b674..1175200f1f9a 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -38,9 +38,10 @@ Python 3.12): 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 +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. @@ -72,7 +73,7 @@ and earlier, but also supported on newer Python versions): 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. + 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. You can think of @@ -310,6 +311,9 @@ inferred by mypy. This is not valid: 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 @@ -519,8 +523,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]``. From 0032acd7b618b897e03da969fe546201aceca5ef Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 25 Sep 2024 12:01:54 +0100 Subject: [PATCH 13/13] Update more references to point to collections.abc --- docs/source/generics.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index 1175200f1f9a..4eb6463e4bd4 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -202,11 +202,12 @@ Here is the above example using the legacy syntax (Python 3.11 and earlier): .. 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 `. When using the legacy syntax, :py:class:`Generic ` can be omitted from bases if there are @@ -253,7 +254,7 @@ Functions can also be generic, i.e. they can have type parameters (Python 3.12 s .. code-block:: python - from typing Sequence + from collections.abc import Sequence # A generic function! def first[T](seq: Sequence[T]) -> T: @@ -570,7 +571,7 @@ Let us illustrate this by few simple examples: arbitrary shape (not just triangles), everything still works. * ``list`` is an invariant generic type. Naively, one would think - that it is covariant, like :py:class:`~typing.Sequence` above, but consider this code: + that it is covariant, like :py:class:`~collections.abc.Sequence` above, but consider this code: .. code-block:: python