Skip to content

Commit ac6c3de

Browse files
gh-91243: Add typing.Required and NotRequired (PEP 655) (GH-32419)
I talked to @davidfstr and I offered to implement the runtime part of PEP 655 to make sure we can get it in before the feature freeze. We're going to defer the documentation to a separate PR, because it can wait until after the feature freeze. The runtime implementation conveniently already exists in typing-extensions, so I largely copied that. Co-authored-by: David Foster <[email protected]>
1 parent 474fdbe commit ac6c3de

File tree

4 files changed

+241
-6
lines changed

4 files changed

+241
-6
lines changed

Lib/test/_typed_dict_helper.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@
66
77
class Bar(_typed_dict_helper.Foo, total=False):
88
b: int
9+
10+
In addition, it uses multiple levels of Annotated to test the interaction
11+
between the __future__ import, Annotated, and Required.
912
"""
1013

1114
from __future__ import annotations
1215

13-
from typing import Optional, TypedDict
16+
from typing import Annotated, Optional, Required, TypedDict
1417

1518
OptionalIntType = Optional[int]
1619

1720
class Foo(TypedDict):
1821
a: OptionalIntType
22+
23+
class VeryAnnotated(TypedDict, total=False):
24+
a: Annotated[Annotated[Annotated[Required[int], "a"], "b"], "c"]

Lib/test/test_typing.py

+173-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from typing import reveal_type
2424
from typing import no_type_check, no_type_check_decorator
2525
from typing import Type
26-
from typing import NamedTuple, TypedDict
26+
from typing import NamedTuple, NotRequired, Required, TypedDict
2727
from typing import IO, TextIO, BinaryIO
2828
from typing import Pattern, Match
2929
from typing import Annotated, ForwardRef
@@ -3993,6 +3993,26 @@ class Options(TypedDict, total=False):
39933993
log_level: int
39943994
log_path: str
39953995

3996+
class TotalMovie(TypedDict):
3997+
title: str
3998+
year: NotRequired[int]
3999+
4000+
class NontotalMovie(TypedDict, total=False):
4001+
title: Required[str]
4002+
year: int
4003+
4004+
class AnnotatedMovie(TypedDict):
4005+
title: Annotated[Required[str], "foobar"]
4006+
year: NotRequired[Annotated[int, 2000]]
4007+
4008+
class DeeplyAnnotatedMovie(TypedDict):
4009+
title: Annotated[Annotated[Required[str], "foobar"], "another level"]
4010+
year: NotRequired[Annotated[int, 2000]]
4011+
4012+
class WeirdlyQuotedMovie(TypedDict):
4013+
title: Annotated['Annotated[Required[str], "foobar"]', "another level"]
4014+
year: NotRequired['Annotated[int, 2000]']
4015+
39964016
class HasForeignBaseClass(mod_generics_cache.A):
39974017
some_xrepr: 'XRepr'
39984018
other_a: 'mod_generics_cache.A'
@@ -4280,6 +4300,36 @@ def test_top_level_class_var(self):
42804300
):
42814301
get_type_hints(ann_module6)
42824302

4303+
def test_get_type_hints_typeddict(self):
4304+
self.assertEqual(get_type_hints(TotalMovie), {'title': str, 'year': int})
4305+
self.assertEqual(get_type_hints(TotalMovie, include_extras=True), {
4306+
'title': str,
4307+
'year': NotRequired[int],
4308+
})
4309+
4310+
self.assertEqual(get_type_hints(AnnotatedMovie), {'title': str, 'year': int})
4311+
self.assertEqual(get_type_hints(AnnotatedMovie, include_extras=True), {
4312+
'title': Annotated[Required[str], "foobar"],
4313+
'year': NotRequired[Annotated[int, 2000]],
4314+
})
4315+
4316+
self.assertEqual(get_type_hints(DeeplyAnnotatedMovie), {'title': str, 'year': int})
4317+
self.assertEqual(get_type_hints(DeeplyAnnotatedMovie, include_extras=True), {
4318+
'title': Annotated[Required[str], "foobar", "another level"],
4319+
'year': NotRequired[Annotated[int, 2000]],
4320+
})
4321+
4322+
self.assertEqual(get_type_hints(WeirdlyQuotedMovie), {'title': str, 'year': int})
4323+
self.assertEqual(get_type_hints(WeirdlyQuotedMovie, include_extras=True), {
4324+
'title': Annotated[Required[str], "foobar", "another level"],
4325+
'year': NotRequired[Annotated[int, 2000]],
4326+
})
4327+
4328+
self.assertEqual(get_type_hints(_typed_dict_helper.VeryAnnotated), {'a': int})
4329+
self.assertEqual(get_type_hints(_typed_dict_helper.VeryAnnotated, include_extras=True), {
4330+
'a': Annotated[Required[int], "a", "b", "c"]
4331+
})
4332+
42834333

42844334
class GetUtilitiesTestCase(TestCase):
42854335
def test_get_origin(self):
@@ -4305,6 +4355,8 @@ class C(Generic[T]): pass
43054355
self.assertIs(get_origin(list | str), types.UnionType)
43064356
self.assertIs(get_origin(P.args), P)
43074357
self.assertIs(get_origin(P.kwargs), P)
4358+
self.assertIs(get_origin(Required[int]), Required)
4359+
self.assertIs(get_origin(NotRequired[int]), NotRequired)
43084360

43094361
def test_get_args(self):
43104362
T = TypeVar('T')
@@ -4342,6 +4394,8 @@ class C(Generic[T]): pass
43424394
self.assertEqual(get_args(Callable[Concatenate[int, P], int]),
43434395
(Concatenate[int, P], int))
43444396
self.assertEqual(get_args(list | str), (list, str))
4397+
self.assertEqual(get_args(Required[int]), (int,))
4398+
self.assertEqual(get_args(NotRequired[int]), (int,))
43454399

43464400

43474401
class CollectionsAbcTests(BaseTestCase):
@@ -5299,6 +5353,32 @@ class Cat(Animal):
52995353
'voice': str,
53005354
}
53015355

5356+
def test_required_notrequired_keys(self):
5357+
self.assertEqual(NontotalMovie.__required_keys__,
5358+
frozenset({"title"}))
5359+
self.assertEqual(NontotalMovie.__optional_keys__,
5360+
frozenset({"year"}))
5361+
5362+
self.assertEqual(TotalMovie.__required_keys__,
5363+
frozenset({"title"}))
5364+
self.assertEqual(TotalMovie.__optional_keys__,
5365+
frozenset({"year"}))
5366+
5367+
self.assertEqual(_typed_dict_helper.VeryAnnotated.__required_keys__,
5368+
frozenset())
5369+
self.assertEqual(_typed_dict_helper.VeryAnnotated.__optional_keys__,
5370+
frozenset({"a"}))
5371+
5372+
self.assertEqual(AnnotatedMovie.__required_keys__,
5373+
frozenset({"title"}))
5374+
self.assertEqual(AnnotatedMovie.__optional_keys__,
5375+
frozenset({"year"}))
5376+
5377+
self.assertEqual(WeirdlyQuotedMovie.__required_keys__,
5378+
frozenset({"title"}))
5379+
self.assertEqual(WeirdlyQuotedMovie.__optional_keys__,
5380+
frozenset({"year"}))
5381+
53025382
def test_multiple_inheritance(self):
53035383
class One(TypedDict):
53045384
one: int
@@ -5399,6 +5479,98 @@ def test_get_type_hints(self):
53995479
)
54005480

54015481

5482+
class RequiredTests(BaseTestCase):
5483+
5484+
def test_basics(self):
5485+
with self.assertRaises(TypeError):
5486+
Required[NotRequired]
5487+
with self.assertRaises(TypeError):
5488+
Required[int, str]
5489+
with self.assertRaises(TypeError):
5490+
Required[int][str]
5491+
5492+
def test_repr(self):
5493+
self.assertEqual(repr(Required), 'typing.Required')
5494+
cv = Required[int]
5495+
self.assertEqual(repr(cv), 'typing.Required[int]')
5496+
cv = Required[Employee]
5497+
self.assertEqual(repr(cv), f'typing.Required[{__name__}.Employee]')
5498+
5499+
def test_cannot_subclass(self):
5500+
with self.assertRaises(TypeError):
5501+
class C(type(Required)):
5502+
pass
5503+
with self.assertRaises(TypeError):
5504+
class C(type(Required[int])):
5505+
pass
5506+
with self.assertRaises(TypeError):
5507+
class C(Required):
5508+
pass
5509+
with self.assertRaises(TypeError):
5510+
class C(Required[int]):
5511+
pass
5512+
5513+
def test_cannot_init(self):
5514+
with self.assertRaises(TypeError):
5515+
Required()
5516+
with self.assertRaises(TypeError):
5517+
type(Required)()
5518+
with self.assertRaises(TypeError):
5519+
type(Required[Optional[int]])()
5520+
5521+
def test_no_isinstance(self):
5522+
with self.assertRaises(TypeError):
5523+
isinstance(1, Required[int])
5524+
with self.assertRaises(TypeError):
5525+
issubclass(int, Required)
5526+
5527+
5528+
class NotRequiredTests(BaseTestCase):
5529+
5530+
def test_basics(self):
5531+
with self.assertRaises(TypeError):
5532+
NotRequired[Required]
5533+
with self.assertRaises(TypeError):
5534+
NotRequired[int, str]
5535+
with self.assertRaises(TypeError):
5536+
NotRequired[int][str]
5537+
5538+
def test_repr(self):
5539+
self.assertEqual(repr(NotRequired), 'typing.NotRequired')
5540+
cv = NotRequired[int]
5541+
self.assertEqual(repr(cv), 'typing.NotRequired[int]')
5542+
cv = NotRequired[Employee]
5543+
self.assertEqual(repr(cv), f'typing.NotRequired[{__name__}.Employee]')
5544+
5545+
def test_cannot_subclass(self):
5546+
with self.assertRaises(TypeError):
5547+
class C(type(NotRequired)):
5548+
pass
5549+
with self.assertRaises(TypeError):
5550+
class C(type(NotRequired[int])):
5551+
pass
5552+
with self.assertRaises(TypeError):
5553+
class C(NotRequired):
5554+
pass
5555+
with self.assertRaises(TypeError):
5556+
class C(NotRequired[int]):
5557+
pass
5558+
5559+
def test_cannot_init(self):
5560+
with self.assertRaises(TypeError):
5561+
NotRequired()
5562+
with self.assertRaises(TypeError):
5563+
type(NotRequired)()
5564+
with self.assertRaises(TypeError):
5565+
type(NotRequired[Optional[int]])()
5566+
5567+
def test_no_isinstance(self):
5568+
with self.assertRaises(TypeError):
5569+
isinstance(1, NotRequired[int])
5570+
with self.assertRaises(TypeError):
5571+
issubclass(int, NotRequired)
5572+
5573+
54025574
class IOTests(BaseTestCase):
54035575

54045576
def test_io(self):

Lib/typing.py

+59-4
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,11 @@ def _idfunc(_, x):
132132
'no_type_check',
133133
'no_type_check_decorator',
134134
'NoReturn',
135+
'NotRequired',
135136
'overload',
136137
'ParamSpecArgs',
137138
'ParamSpecKwargs',
139+
'Required',
138140
'reveal_type',
139141
'runtime_checkable',
140142
'Self',
@@ -2262,6 +2264,8 @@ def _strip_annotations(t):
22622264
"""
22632265
if isinstance(t, _AnnotatedAlias):
22642266
return _strip_annotations(t.__origin__)
2267+
if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired):
2268+
return _strip_annotations(t.__args__[0])
22652269
if isinstance(t, _GenericAlias):
22662270
stripped_args = tuple(_strip_annotations(a) for a in t.__args__)
22672271
if stripped_args == t.__args__:
@@ -2786,10 +2790,22 @@ def __new__(cls, name, bases, ns, total=True):
27862790
optional_keys.update(base.__dict__.get('__optional_keys__', ()))
27872791

27882792
annotations.update(own_annotations)
2789-
if total:
2790-
required_keys.update(own_annotation_keys)
2791-
else:
2792-
optional_keys.update(own_annotation_keys)
2793+
for annotation_key, annotation_type in own_annotations.items():
2794+
annotation_origin = get_origin(annotation_type)
2795+
if annotation_origin is Annotated:
2796+
annotation_args = get_args(annotation_type)
2797+
if annotation_args:
2798+
annotation_type = annotation_args[0]
2799+
annotation_origin = get_origin(annotation_type)
2800+
2801+
if annotation_origin is Required:
2802+
required_keys.add(annotation_key)
2803+
elif annotation_origin is NotRequired:
2804+
optional_keys.add(annotation_key)
2805+
elif total:
2806+
required_keys.add(annotation_key)
2807+
else:
2808+
optional_keys.add(annotation_key)
27932809

27942810
tp_dict.__annotations__ = annotations
27952811
tp_dict.__required_keys__ = frozenset(required_keys)
@@ -2874,6 +2890,45 @@ class body be required.
28742890
TypedDict.__mro_entries__ = lambda bases: (_TypedDict,)
28752891

28762892

2893+
@_SpecialForm
2894+
def Required(self, parameters):
2895+
"""A special typing construct to mark a key of a total=False TypedDict
2896+
as required. For example:
2897+
2898+
class Movie(TypedDict, total=False):
2899+
title: Required[str]
2900+
year: int
2901+
2902+
m = Movie(
2903+
title='The Matrix', # typechecker error if key is omitted
2904+
year=1999,
2905+
)
2906+
2907+
There is no runtime checking that a required key is actually provided
2908+
when instantiating a related TypedDict.
2909+
"""
2910+
item = _type_check(parameters, f'{self._name} accepts only a single type.')
2911+
return _GenericAlias(self, (item,))
2912+
2913+
2914+
@_SpecialForm
2915+
def NotRequired(self, parameters):
2916+
"""A special typing construct to mark a key of a TypedDict as
2917+
potentially missing. For example:
2918+
2919+
class Movie(TypedDict):
2920+
title: str
2921+
year: NotRequired[int]
2922+
2923+
m = Movie(
2924+
title='The Matrix', # typechecker error if key is omitted
2925+
year=1999,
2926+
)
2927+
"""
2928+
item = _type_check(parameters, f'{self._name} accepts only a single type.')
2929+
return _GenericAlias(self, (item,))
2930+
2931+
28772932
class NewType:
28782933
"""NewType creates simple unique types with almost zero
28792934
runtime overhead. NewType(name, tp) is considered a subtype of tp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Implement ``typing.Required`` and ``typing.NotRequired`` (:pep:`655`). Patch
2+
by Jelle Zijlstra.

0 commit comments

Comments
 (0)