From 21ea48c4ec682e727d4ebddcef7102fdccd8011e Mon Sep 17 00:00:00 2001 From: franekmagiera Date: Mon, 1 May 2023 19:22:55 +0200 Subject: [PATCH 1/4] Update the cheatsheet for functions and keyword argument typing --- docs/source/cheat_sheet_py3.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/source/cheat_sheet_py3.rst b/docs/source/cheat_sheet_py3.rst index 9533484e938b..80b3ffbb3845 100644 --- a/docs/source/cheat_sheet_py3.rst +++ b/docs/source/cheat_sheet_py3.rst @@ -88,7 +88,7 @@ Functions .. code-block:: python - from typing import Callable, Iterator, Union, Optional + from typing import Callable, Iterator, Union, Unpack, Optional, TypedDict # This is how you annotate a function definition def stringify(num: int) -> str: @@ -146,6 +146,19 @@ Functions reveal_type(kwargs) # Revealed type is "dict[str, str]" request = make_request(*args, **kwargs) return self.do_api_query(request) + + # For more preise keyword typing, you can use `Unpack` along with a + # `TypedDict` + class Options(TypedDict): + timeout: int + on_error: Callable[[int], None] + + # This function expects a keyword argument `timeout` of type `int` and a + # keyword argument `on_error` that is a `Callable[[int], None]` + def call(**options: Unpack[Options]) -> str: + reveal_type(options) # Revealed type is "Options" + request = create_request(options['timeout'], options['on_error']) + return self.do_api_query(request) Classes ******* From 5952e018adafd657a1c8fc5e414430bc932f4196 Mon Sep 17 00:00:00 2001 From: franekmagiera Date: Tue, 2 May 2023 18:31:24 +0200 Subject: [PATCH 2/4] Update the TypedDict docs Updated and extended the docs section on mixing required non-required items in a TypedDict. Introduced the use of Required and NotRequired type qualifiers. --- docs/source/typed_dict.rst | 75 ++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/docs/source/typed_dict.rst b/docs/source/typed_dict.rst index 19a717d7feb7..27ddd5a5bbba 100644 --- a/docs/source/typed_dict.rst +++ b/docs/source/typed_dict.rst @@ -216,18 +216,17 @@ Now ``BookBasedMovie`` has keys ``name``, ``year`` and ``based_on``. Mixing required and non-required items -------------------------------------- -In addition to allowing reuse across ``TypedDict`` types, inheritance also allows -you to mix required and non-required (using ``total=False``) items -in a single ``TypedDict``. Example: +Special type qualifiers ``Required[T]`` and ``NotRequired[T]`` can be used to +specify required and non-required keys of a ``TypedDict``. .. code-block:: python - class MovieBase(TypedDict): + from typing_extensions import NotRequired + + class Movie(TypedDict): name: str year: int - - class Movie(MovieBase, total=False): - based_on: str + based_on: NotRequired[str] Now ``Movie`` has required keys ``name`` and ``year``, while ``based_on`` can be left out when constructing an object. A ``TypedDict`` with a mix of required @@ -236,6 +235,68 @@ another ``TypedDict`` if all required keys in the other ``TypedDict`` are requir first ``TypedDict``, and all non-required keys of the other ``TypedDict`` are also non-required keys in the first ``TypedDict``. +Depending on the totality of the ``TypedDict`` either ``Required`` or +``NotRequired`` can be explicitly used on some of the keys. For instance, the +``Movie`` type from the example above could be also defined as: + +.. code-block:: python + + from typing_extensions import Required + + class Movie(TypedDict, total=False): + name: Required[str] + year: Required[int] + based_on: str + +and the two definitions would be equivalent. + +As a rule of thumb, if more keys of a particular ``TypedDict`` are required +than not, construct the new type with the ``total`` parameter set to ``True`` +and qualify the non required keys using ``NotRequired``. Otherwise, construct +the new type with the ``total`` parameter set to ``False`` and qualify the +required keys with ``Required``. + +Using ``Required`` with a total ``TypedDict`` and using ``NotRequired`` with a +partial ``TypedDict`` is redundant. However, it is allowed and can be used to +qualify the required and not required keys explicitly. + +If a particular key can accept ``None``, it is recommended to avoid mixing +``Optional`` with either ``Required`` or ``NotRequired`` and use the +``TYPE|None`` notation instead: + +.. code-block:: python + + # Preferred approach. + class Car(TypedDict): + model: str + owner: NotRequired[str | None] + + # Not recommended. + class Car(TypedDict): + model: str + owner: NotRequired[Optional[str]] + +The ``Required`` and ``NotRequired`` type qualifiers are supported since +Python 3.11. For earlier versions of Python, both ``TypedDict`` and +``Required`` and ``NotRequired`` type qualifiers have to be imported from the +``typing_extensions``. + +Mixing required and not required items in a single ``TypedDict`` can also be +achieved with inheritance, by using a mix of total and partial typed +dictionaries: + +.. code-block:: python + + class MovieBase(TypedDict): + name: str + year: int + + class Movie(MovieBase, total=False): + based_on: str + +A ``Movie`` type defined like this would be equivalent to the other ``Movie`` +types defined in this section. + Unions of TypedDicts -------------------- From a1c696c8e279f7c427df1c3d00fe5e8ff4b6a676 Mon Sep 17 00:00:00 2001 From: franekmagiera Date: Tue, 2 May 2023 18:58:45 +0200 Subject: [PATCH 3/4] Fix typo and remove blank character --- docs/source/cheat_sheet_py3.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/cheat_sheet_py3.rst b/docs/source/cheat_sheet_py3.rst index 80b3ffbb3845..821c94c26b47 100644 --- a/docs/source/cheat_sheet_py3.rst +++ b/docs/source/cheat_sheet_py3.rst @@ -147,10 +147,10 @@ Functions request = make_request(*args, **kwargs) return self.do_api_query(request) - # For more preise keyword typing, you can use `Unpack` along with a + # For more precise keyword typing, you can use `Unpack` along with a # `TypedDict` class Options(TypedDict): - timeout: int + timeout: int on_error: Callable[[int], None] # This function expects a keyword argument `timeout` of type `int` and a From 535e835d6dd706a6cecee28380c82baa1c776f83 Mon Sep 17 00:00:00 2001 From: franekmagiera Date: Tue, 2 May 2023 18:59:43 +0200 Subject: [PATCH 4/4] Add a section on using Unpack to TypedDict docs --- docs/source/typed_dict.rst | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/source/typed_dict.rst b/docs/source/typed_dict.rst index 27ddd5a5bbba..c770a3c3f0e9 100644 --- a/docs/source/typed_dict.rst +++ b/docs/source/typed_dict.rst @@ -309,3 +309,32 @@ section of the docs has a full description with an example, but in short, you wi need to give each TypedDict the same key where each value has a unique :ref:`Literal type `. Then, check that key to distinguish between your TypedDicts. + +Typing function's ``**kwargs`` +------------------------------ + +``TypedDict`` can be used along with ``Unpack`` to precisely type annotate the +``**kwargs`` in a function signature: + +.. code-block:: python + + from typing_extensions import Unpack + + class Options(TypedDict): + timeout: int + on_error: Callable[[int], None] + + def handle_error(code: int) -> None: ... + + # This function expects a keyword argument `timeout` of type `int` and a + # keyword argument `on_error` that is a `Callable[[int], None]` + def call(**options: Unpack[Options]): ... + + call(timeout=5, on_error=handle_error) + +That way, both keyword names and their types are checked during static type +analysis. + +Note that ``Unpack`` has to be used, as annotating ``**kwargs`` directly with a +certain type means, that the function expects all of its keyword arguments +captured by ``**kwargs`` to be of that type.