To define a TypedDict where most keys are required and some are
Hey David, Thanks for the useful PEP! Users will find it easier to use a single TypedDict with Required/NotRequired than to use TypedDict inheritance. I've rarely seen anyone use the latter. # Type Compatibility with PEP 589 TypedDicts Could you mention how type compatibility checks work for a PEP-655-style TypedDict against a PEP-589-style TypedDict (and vice versa)? For example, is a single-class Movie with a Required `name` and NotRequired `year` compatible with the inherited `Movie` in your Motivation example? I'm guessing the answer is "obviously yes", but it's probably worth explicitly laying out in the PEP. We could do this by saying that the following two TypedDicts are compatible with each other: ``` # old style class MovieBase(TypedDict): name: str class Movie1(MovieBase, total=False): year: int # new style class Movie2(TypedDict): name: Required[str] year: NotRequired[int] def expect_movie1(movie: Movie1) -> None: ... movie2: Movie2 expect_movie1(movie2) # and vice versa ``` Similarly, is there any difference in runtime or type-checking behavior between a TypedDict with a redundant `name: Required[str]` and one with `name: str`? I'm guessing not but might be worth specifying. Otherwise, there might be questions like, do they generate the same constructor for the Movie class? Once we specify that they are equivalent for type-checking purposes, type checker implementers (such as me on Pyre) can just desugar the latter to the former without any issues. # Other comments A couple of quick readability comments: **Motivation section**: You've shown the "Before" example. Could you show the "After" example (i.e., the same code using your PEP's feature)? That would make it easy for people who just want to get the gist of the PEP without diving into the Specification. Would also be great if you can also demonstrate both Required and NotRequired, but this is... not required :) **Specification**: Could you nest the three sections for "Interaction with get_type_hints/get_origin/__required_keys" under a "Runtime Behavior" section? That way, users who care just about typing can skip that section. Is it valid to use `Required` in an inherited TypedDict? **How to Teach This section**: potentially-missing, define a single TypedDict as normal and mark those few keys that are potentially-missing with NotRequired[]. Could you clarify that "define a single TypedDict as normal" means "without a `total` keyword"? Not obvious otherwise. Best, On Fri, Jan 28, 2022 at 8:07 AM David Foster <davidfstr@gmail.com> wrote:
Recent feedback has been integrated. Please see latest PEP text at the bottom of this email. I think this draft is likely stable enough to submit to the Steering Council.
Major changes since last revision:
* Integrate feedback RE how typing.TypedDict will (not) support typing_extensions.Required in certain Python versions. In particular update the How to Teach This section
* Explain why `Annotated[Required[...], ...]` is allowed.
See the full diff at: https://github.com/python/peps/pull/2287
-- David Foster | Seattle, WA, USA Contributor to TypedDict support for mypy
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
PEP: 655 Title: Marking individual TypedDict items as required or potentially-missing Author: David Foster <david at dafoster.net> Sponsor: Guido van Rossum <guido at python.org> Discussions-To: typing-sig at python.org Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 30-Jan-2021 Python-Version: 3.11 Post-History: 31-Jan-2021, 11-Feb-2021, 20-Feb-2021, 26-Feb-2021, 17-Jan-2022, 28-Jan-2022
Abstract ========
:pep:`589` defines syntax for declaring a TypedDict with all required keys and syntax for defining a TypedDict with :pep:`all potentially-missing keys <589#totality>` however it does not provide any syntax to declare some keys as required and others as potentially-missing. This PEP introduces two new syntaxes: ``Required[]`` which can be used on individual items of a TypedDict to mark them as required, and ``NotRequired[]`` which can be used on individual items to mark them as potentially-missing.
Motivation ==========
It is not uncommon to want to define a TypedDict with some keys that are required and others that are potentially-missing. Currently the only way to define such a TypedDict is to declare one TypedDict with one value for ``total`` and then inherit it from another TypedDict with a different value for ``total``:
::
class _MovieBase(TypedDict): # implicitly total=True title: str
class Movie(_MovieBase, total=False): year: int
Having to declare two different TypedDict types for this purpose is cumbersome.
Rationale =========
One might think it unusual to propose syntax that prioritizes marking *required* keys rather than syntax for *potentially-missing* keys, as is customary in other languages like TypeScript:
::
interface Movie { title: string; year?: number; // ? marks potentially-missing keys }
The difficulty is that the best word for marking a potentially-missing key, ``Optional[]``, is already used in Python for a completely different purpose: marking values that could be either of a particular type or ``None``. In particular the following does not work:
::
class Movie(TypedDict): ... year: Optional[int] # means int|None, not potentially-missing!
Attempting to use any synonym of “optional” to mark potentially-missing keys (like ``Missing[]``) would be too similar to ``Optional[]`` and be easy to confuse with it.
Thus it was decided to focus on positive-form phrasing for required keys instead, which is straightforward to spell as ``Required[]``.
Nevertheless it is common for folks wanting to extend a regular (``total=True``) TypedDict to only want to add a small number of potentially-missing keys, which necessitates a way to mark keys that are *not* required and potentially-missing, and so we also allow the ``NotRequired[]`` form for that case.
Specification =============
The ``typing.Required`` type qualifier is used to indicate that a variable declared in a TypedDict definition is a required key:
::
class Movie(TypedDict, total=False): title: Required[str] year: int
Additionally the ``typing.NotRequired`` type qualifier is used to indicate that a variable declared in a TypedDict definition is a potentially-missing key:
::
class Movie(TypedDict): # implicitly total=True title: str year: NotRequired[int]
It is an error to use ``Required[]`` or ``NotRequired[]`` in any location that is not an item of a TypedDict.
It is valid to use ``Required[]`` and ``NotRequired[]`` even for items where it is redundant, to enable additional explicitness if desired:
::
class Movie(TypedDict): title: Required[str] # redundant year: NotRequired[int]
It is an error to use both ``Required[]`` and ``NotRequired[]`` at the same time:
::
class Movie(TypedDict): title: str year: NotRequired[Required[int]] # ERROR
The :pep:`alternative syntax <589#alternative-syntax>` for TypedDict also supports ``Required[]`` and ``NotRequired[]``:
::
Movie = TypedDict('Movie', {'name': str, 'year': NotRequired[int]})
Interaction with ``Annotated[]`` -----------------------------------
``Required[]`` and ``NotRequired[]`` can be used with ``Annotated[]``, in any nesting order:
::
class Movie(TypedDict): title: str year: NotRequired[Annotated[int, ValueRange(-9999, 9999)]] # ok
::
class Movie(TypedDict): title: str year: Annotated[NotRequired[int], ValueRange(-9999, 9999)] # ok
In particular allowing ``Annotated[]`` to be the outermost annotation for an item allows better interoperability with non-typing uses of annotations, which may always want ``Annotated[]`` as the outermost annotation. [3]_
Interaction with ``get_type_hints()`` -------------------------------------
``typing.get_type_hints(...)`` applied to a TypedDict will by default strip out any ``Required[]`` or ``NotRequired[]`` type qualifiers, since these qualifiers are expected to be inconvenient for code casually introspecting type annotations.
``typing.get_type_hints(..., include_extras=True)`` however *will* retain ``Required[]`` and ``NotRequired[]`` type qualifiers, for advanced code introspecting type annotations that wishes to preserve *all* annotations in the original source:
::
class Movie(TypedDict): title: str year: NotRequired[int]
assert get_type_hints(Movie) == \ {'title': str, 'year': int} assert get_type_hints(Movie, include_extras=True) == \ {'title': str, 'year': NotRequired[int]}
Interaction with ``get_origin()`` and ``get_args()`` ----------------------------------------------------
``typing.get_origin()`` and ``typing.get_args()`` will be updated to recognize ``Required[]`` and ``NotRequired[]``:
::
assert get_origin(Required[int]) is Required assert get_args(Required[int]) == (int,)
assert get_origin(NotRequired[int]) is NotRequired assert get_args(NotRequired[int]) == (int,)
Interaction with ``__required_keys__`` and ``__optional_keys__`` ----------------------------------------------------------------
An item marked with ``Required[]`` will always appear in the ``__required_keys__`` for its enclosing TypedDict. Similarly an item marked with ``NotRequired[]`` will always appear in ``__optional_keys__``.
::
assert Movie.__required_keys__ == frozenset({'title'}) assert Movie.__optional_keys__ == frozenset({'year'})
Backwards Compatibility =======================
No backward incompatible changes are made by this PEP.
How to Teach This =================
To define a TypedDict where most keys are required and some are potentially-missing, define a single TypedDict as normal and mark those few keys that are potentially-missing with ``NotRequired[]``.
To define a TypedDict where most keys are potentially-missing and a few are required, define a ``total=False`` TypedDict and mark those few keys that are required with ``Required[]``.
If some items accept ``None`` in addition to a regular value, it is recommended that the ``TYPE|None`` syntax be preferred over ``Optional[TYPE]`` for marking such item values, to avoid using ``Required[]`` or ``NotRequired[]`` alongside ``Optional[]`` within the same TypedDict definition:
Yes:
::
from __future__ import annotations # for Python 3.7-3.9
class Dog(TypedDict): name: str owner: NotRequired[str|None]
Okay (required for Python 3.5.3-3.6):
::
class Dog(TypedDict): name: str owner: 'NotRequired[str|None]'
No:
::
class Dog(TypedDict): name: str # ick; avoid using both Optional and NotRequired owner: NotRequired[Optional[str]]
Usage in Python <3.11 ---------------------
If your code supports Python <3.11 and wishes to use ``Required[]`` or ``NotRequired[]`` then it should use ``typing_extensions.TypedDict`` rather than ``typing.TypedDict`` because the latter will not understand ``(Not)Required[]``. In particular ``__required_keys__`` and ``__optional_keys__`` on the resulting TypedDict type will not be correct:
Yes (Python 3.11+ only):
::
from typing import NotRequired, TypedDict
class Dog(TypedDict): name: str owner: NotRequired[str|None]
Yes (Python <3.11 and 3.11+):
::
from __future__ import annotations # for Python 3.7-3.9
from typing_extensions import NotRequired, TypedDict # for Python <3.11 with (Not)Required
class Dog(TypedDict): name: str owner: NotRequired[str|None]
No (Python <3.11 and 3.11+):
::
from typing import TypedDict # oops: should import from typing_extensions instead from typing_extensions import NotRequired
class Movie(TypedDict): title: str year: NotRequired[int]
assert Movie.__required_keys__ == frozenset({'title', 'year'}) # yikes assert Movie.__optional_keys__ == frozenset() # yikes
Reference Implementation ========================
The `mypy <http://www.mypy-lang.org/>`__ `0.930 <https://mypy-lang.blogspot.com/2021/12/mypy-0930-released.html
`__, `pyright <https://github.com/Microsoft/pyright>`__ `1.1.117 < https://github.com/microsoft/pyright/commit/7ed245b1845173090c6404e49912e8cb... `__, and `pyanalyze <https://github.com/quora/pyanalyze>`__ `0.4.0 < https://pyanalyze.readthedocs.io/en/latest/changelog.html#version-0-4-0-nove... `__ type checkers support ``Required`` and ``NotRequired``.
A reference implementation of the runtime component is provided in the `typing_extensions <https://github.com/python/typing/tree/master/typing_extensions>`__ module.
Rejected Ideas ==============
Special syntax around the *key* of a TypedDict item ---------------------------------------------------
::
class MyThing(TypedDict): opt1?: str # may not exist, but if exists, value is string opt2: Optional[str] # always exists, but may have None value
This syntax would require Python grammar changes and it is not believed that marking TypedDict items as required or potentially-missing would meet the high bar required to make such grammar changes.
::
class MyThing(TypedDict): Optional[opt1]: str # may not exist, but if exists, value is string opt2: Optional[str] # always exists, but may have None value
This syntax causes ``Optional[]`` to take on different meanings depending on where it is positioned, which is inconsistent and confusing.
Also, “let’s just not put funny syntax before the colon.” [1]_
Marking required or potentially-missing keys with an operator -------------------------------------------------------------
We could use unary ``+`` as shorthand to mark a required key, unary ``-`` to mark a potentially-missing key, or unary ``~`` to mark a key with opposite-of-normal totality:
::
class MyThing(TypedDict, total=False): req1: +int # + means a required key, or Required[] opt1: str req2: +float
class MyThing(TypedDict): req1: int opt1: -str # - means a potentially-missing key, or NotRequired[] req2: float
class MyThing(TypedDict): req1: int opt1: ~str # ~ means a opposite-of-normal-totality key req2: float
Such operators could be implemented on ``type`` via the ``__pos__``, ``__neg__`` and ``__invert__`` special methods without modifying the grammar.
It was decided that it would be prudent to introduce longform syntax (i.e. ``Required[]`` and ``NotRequired[]``) before introducing any shortform syntax. Future PEPs may reconsider introducing this or other shortform syntax options.
Note when reconsidering introducing this shortform syntax that ``+``, ``-``, and ``~`` already have existing meanings in the Python typing world: covariant, contravariant, and invariant:
::
>>> from typing import TypeVar >>> (TypeVar('T', covariant=True), TypeVar('U', contravariant=True), TypeVar('V')) (+T, -U, ~V)
Marking absence of a value with a special constant --------------------------------------------------
We could introduce a new type-level constant which signals the absence of a value when used as a union member, similar to JavaScript’s ``undefined`` type, perhaps called ``Missing``:
::
class MyThing(TypedDict): req1: int opt1: str|Missing req2: float
Such a ``Missing`` constant could also be used for other scenarios such as the type of a variable which is only conditionally defined:
::
class MyClass: attr: int|Missing
def __init__(self, set_attr: bool) -> None: if set_attr: self.attr = 10
::
def foo(set_attr: bool) -> None: if set_attr: attr = 10 reveal_type(attr) # int|Missing
Misalignment with how unions apply to values ''''''''''''''''''''''''''''''''''''''''''''
However this use of ``...|Missing``, equivalent to ``Union[..., Missing]``, doesn’t align well with what a union normally means: ``Union[...]`` always describes the type of a *value* that is present. By contrast missingness or non-totality is a property of a *variable* instead. Current precedent for marking properties of a variable include ``Final[...]`` and ``ClassVar[...]``, which the proposal for ``Required[...]`` is aligned with.
Misalignment with how unions are subdivided '''''''''''''''''''''''''''''''''''''''''''
Furthermore the use of ``Union[..., Missing]`` doesn’t align with the usual ways that union values are broken down: Normally you can eliminate components of a union type using ``isinstance`` checks:
::
class Packet: data: Union[str, bytes]
def send_data(packet: Packet) -> None: if isinstance(packet.data, str): reveal_type(packet.data) # str packet_bytes = packet.data.encode('utf-8') else: reveal_type(packet.data) # bytes packet_bytes = packet.data socket.send(packet_bytes)
However if we were to allow ``Union[..., Missing]`` you’d either have to eliminate the ``Missing`` case with ``hasattr`` for object attributes:
::
class Packet: data: Union[str, Missing]
def send_data(packet: Packet) -> None: if hasattr(packet, 'data'): reveal_type(packet.data) # str packet_bytes = packet.data.encode('utf-8') else: reveal_type(packet.data) # Missing? error? packet_bytes = b'' socket.send(packet_bytes)
or a check against ``locals()`` for local variables:
::
def send_data(packet_data: Optional[str]) -> None: packet_bytes: Union[str, Missing] if packet_data is not None: packet_bytes = packet.data.encode('utf-8')
if 'packet_bytes' in locals(): reveal_type(packet_bytes) # bytes socket.send(packet_bytes) else: reveal_type(packet_bytes) # Missing? error?
or a check via other means, such as against ``globals()`` for global variables:
::
warning: Union[str, Missing] import sys if sys.version_info < (3, 6): warning = 'Your version of Python is unsupported!'
if 'warning' in globals(): reveal_type(warning) # str print(warning) else: reveal_type(warning) # Missing? error?
Weird and inconsistent. ``Missing`` is not really a value at all; it’s an absence of definition and such an absence should be treated specially.
Difficult to implement ''''''''''''''''''''''
Eric Traut from the Pyright type checker team has stated that implementing a ``Union[..., Missing]``-style syntax would be difficult. [2]_
Introduces a second null-like value into Python '''''''''''''''''''''''''''''''''''''''''''''''
Defining a new ``Missing`` type-level constant would be very close to introducing a new ``Missing`` value-level constant at runtime, creating a second null-like runtime value in addition to ``None``. Having two different null-like constants in Python (``None`` and ``Missing``) would be confusing. Many newcomers to JavaScript already have difficulty distinguishing between its analogous constants ``null`` and ``undefined``.
Replace Optional with Nullable. Repurpose Optional to mean “optional item”. ---------------------------------------------------------------------------
``Optional[]`` is too ubiquitous to deprecate. Although use of it *may* fade over time in favor of the ``T|None`` syntax specified by :pep:`604`.
Change Optional to mean “optional item” in certain contexts instead of “nullable”
---------------------------------------------------------------------------------
Consider the use of a special flag on a TypedDict definition to alter the interpretation of ``Optional`` inside the TypedDict to mean “optional item” rather than its usual meaning of “nullable”:
::
class MyThing(TypedDict, optional_as_missing=True): req1: int opt1: Optional[str]
or:
::
class MyThing(TypedDict, optional_as_nullable=False): req1: int opt1: Optional[str]
This would add more confusion for users because it would mean that in *some* contexts the meaning of ``Optional[]`` is different than in other contexts, and it would be easy to overlook the flag.
Various synonyms for “potentially-missing item” -----------------------------------------------
- Omittable – too easy to confuse with optional - OptionalItem, OptionalKey – two words; too easy to confuse with optional - MayExist, MissingOk – two words - Droppable – too similar to Rust’s ``Drop``, which means something different - Potential – too vague - Open – sounds like applies to an entire structure rather then to an item - Excludable - Checked
References ==========
.. [1]
https://mail.python.org/archives/list/typing-sig@python.org/message/4I3GPIWD...
.. [2]
https://mail.python.org/archives/list/typing-sig@python.org/message/S2VJSVG6...
.. [3] https://bugs.python.org/issue46491
Copyright =========
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
.. Local Variables: mode: indented-text indent-tabs-mode: nil sentence-end-double-space: t fill-column: 70 coding: utf-8 End:
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: gohanpra@gmail.com
-- S Pradeep Kumar