Skip to content

Commit 66d1fc8

Browse files
alicederynhugovk
andauthored
PEP 705: Clarifications, and define runtime behavior changes (#3526)
Co-authored-by: Hugo van Kemenade <[email protected]>
1 parent bb16334 commit 66d1fc8

File tree

1 file changed

+43
-7
lines changed

1 file changed

+43
-7
lines changed

peps/pep-0705.rst

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ Abstract
2121

2222
:pep:`589` defines the structural type :class:`~typing.TypedDict` for dictionaries with a fixed set of keys.
2323
As ``TypedDict`` is a mutable type, it is difficult to correctly annotate methods which accept read-only parameters in a way that doesn't prevent valid inputs.
24-
This PEP proposes a new type qualifier, ``typing.ReadOnly``, to support these usages.
24+
25+
This PEP proposes a new type qualifier, ``typing.ReadOnly``, to support these usages. It makes no Python grammar changes. Correct usage of read-only keys of TypedDicts is intended to be enforced only by static type checkers, and will not be enforced by Python itself at runtime.
2526

2627
Motivation
2728
==========
@@ -125,7 +126,7 @@ It is possible to work around this issue with generics (as of Python 3.11), but
125126
Rationale
126127
=========
127128

128-
These problems can be resolved by removing the ability to update one or more of the items in a ``TypedDict``. This does not mean the items are immutable: a reference to the underlying dictionary could still exist with a different but compatible type in which those items have mutator operations. As such, these are not "final" items; using this term would risk confusion with final attributes, which are fully immutable. These items are "read-only", and we introduce a new ``typing.ReadOnly`` type qualifier for this purpose.
129+
These problems can be resolved by removing the ability to update one or more of the items in a ``TypedDict``. This does not mean the items are immutable: a reference to the underlying dictionary could still exist with a different but compatible type in which those items have mutator operations. These items are "read-only", and we introduce a new ``typing.ReadOnly`` type qualifier for this purpose.
129130

130131
The ``movie_string`` function in the first motivating example can then be typed as follows::
131132

@@ -324,7 +325,7 @@ In addition to existing type checking rules, type checkers should error if a Typ
324325
a2: A = { "x": 3, "y": 4 }
325326
a1.update(a2) # Type check error: "x" is read-only in A
326327

327-
Unless the declared value is of bottom type::
328+
Unless the declared value is of bottom type (:data:`~typing.Never`)::
328329

329330
class B(TypedDict):
330331
x: NotRequired[typing.Never]
@@ -333,6 +334,8 @@ Unless the declared value is of bottom type::
333334
def update_a(a: A, b: B) -> None:
334335
a.update(b) # Accepted by type checker: "x" cannot be set on b
335336

337+
Note: Nothing will ever match the ``Never`` type, so an item annotated with it must be absent.
338+
336339
Keyword argument typing
337340
-----------------------
338341

@@ -354,6 +357,30 @@ Keyword argument typing
354357

355358
fn: Function = impl # Accepted by type checker: function signatures are identical
356359

360+
Runtime behavior
361+
----------------
362+
363+
``TypedDict`` types will gain two new attributes, ``__readonly_keys__`` and ``__mutable_keys__``, which will be frozensets containing all read-only and non-read-only keys, respectively::
364+
365+
class Example(TypedDict):
366+
a: int
367+
b: ReadOnly[int]
368+
c: int
369+
d: ReadOnly[int]
370+
371+
assert Example.__readonly_keys__ == frozenset({'b', 'd'})
372+
assert Example.__mutable_keys__ == frozenset({'a', 'c'})
373+
374+
``typing.get_type_hints`` will strip out any ``ReadOnly`` type qualifiers, unless ``include_extras`` is ``True``::
375+
376+
assert get_type_hints(Example)['b'] == int
377+
assert get_type_hints(Example, include_extras=True)['b'] == ReadOnly[int]
378+
379+
``typing.get_origin`` and ``typing.get_args`` will be updated to recognize ``ReadOnly``::
380+
381+
assert get_origin(ReadOnly[int]) is ReadOnly
382+
assert get_args(ReadOnly[int]) == (int,)
383+
357384

358385
Backwards compatibility
359386
=======================
@@ -368,18 +395,18 @@ There are no known security consequences arising from this PEP.
368395
How to teach this
369396
=================
370397

371-
Suggestion for changes to the :mod:`typing` module, in line with current practice:
398+
Suggested changes to the :mod:`typing` module documentation, in line with current practice:
372399

373400
* Add this PEP to the others listed.
374401
* Add ``typing.ReadOnly``, linked to TypedDict and this PEP.
375402
* Add the following text to the TypedDict entry:
376403

377-
Individual items can be excluded from mutate operations using ReadOnly, allowing them to be read but not changed. This is useful when the exact type of the value is not known yet, and so modifying it would break structural subtypes. *insert example*
404+
The ``ReadOnly`` type qualifier indicates that an item declared in a ``TypedDict`` definition may be read but not mutated (added, modified or removed). This is useful when the exact type of the value is not known yet, and so modifying it would break structural subtypes. *insert example*
378405

379406
Reference implementation
380407
========================
381408

382-
pyright 1.1.332 fully implements this proposal.
409+
`pyright 1.1.333 fully implements this proposal <https://github.com/microsoft/pyright/releases/tag/1.1.333>`_.
383410

384411
Rejected alternatives
385412
=====================
@@ -399,6 +426,15 @@ Calling the type ``Readonly``
399426

400427
``Read-only`` is generally hyphenated, and it appears to be common convention to put initial caps onto words separated by a dash when converting to CamelCase. This appears consistent with the definition of CamelCase on Wikipedia: CamelCase uppercases the first letter of each word. That said, Python examples or counter-examples, ideally from the core Python libraries, or better explicit guidance on the convention, would be greatly appreciated.
401428

429+
Reusing the ``Final`` annotation
430+
--------------------------------
431+
432+
The :class:`~typing.Final` annotation prevents an attribute from being modified, like the proposed ``ReadOnly`` qualifier does for ``TypedDict`` items. However, it is also documented as preventing redefinition in subclasses too; from :pep:`591`:
433+
434+
The ``typing.Final`` type qualifier is used to indicate that a variable or attribute should not be reassigned, redefined, or overridden.
435+
436+
This does not fit with the intended use of ``ReadOnly``. Rather than introduce confusion by having ``Final`` behave differently in different contexts, we chose to introduce a new qualifier.
437+
402438
A readonly flag
403439
---------------
404440

@@ -422,7 +458,7 @@ However, this led to confusion when inheritance was introduced::
422458
b: B = { "key1": 1, "key2": 2 }
423459
b["key1"] = 4 # Accepted by type checker: "key1" is not read-only
424460

425-
It would be reasonable for someone familiar with ``frozen``, on seeing just the definition of B, to assume that the whole type was read-only. On the other hand, it would be reasonable for someone familiar with ``total`` to assume that read-only only applies to the current type.
461+
It would be reasonable for someone familiar with ``frozen`` (from :mod:`dataclasses`), on seeing just the definition of B, to assume that the whole type was read-only. On the other hand, it would be reasonable for someone familiar with ``total`` to assume that read-only only applies to the current type.
426462

427463
The original proposal attempted to eliminate this ambiguity by making it both a type check and a runtime error to define ``B`` in this way. This was still a source of surprise to people expecting it to work like ``total``.
428464

0 commit comments

Comments
 (0)