Reimplement TypedDict in a similar way to Python (#191)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a424b72..7f2bd90 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Unreleased
+- Align the implementation of `TypedDict` with the implementation in the
+ standard library on Python 3.9 and higher.
+ `typing_extensions.TypedDict` is now a function instead of a class. The
+ private functions `_check_fails`, `_dict_new`, and `_typeddict_new`
+ have been removed. `is_typeddict` now returns `False` when called with
+ `TypedDict` itself as the argument. Patch by Jelle Zijlstra.
- Declare support for Python 3.12. Patch by Jelle Zijlstra.
- Fix tests on Python 3.13, which removes support for creating
`TypedDict` classes through the keyword-argument syntax. Patch by
diff --git a/doc/index.rst b/doc/index.rst
index f076ae3..6049737 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -347,6 +347,12 @@
Support for the ``__orig_bases__`` attribute was added.
+ .. versionchanged:: 4.7.0
+
+ ``TypedDict`` is now a function rather than a class.
+ This brings ``typing_extensions.TypedDict`` closer to the implementation
+ of :py:mod:`typing.TypedDict` on Python 3.9 and higher.
+
.. class:: TypeVar(name, *constraints, bound=None, covariant=False,
contravariant=False, infer_variance=False, default=...)
@@ -680,6 +686,12 @@
.. versionadded:: 4.1.0
+ .. versionchanged:: 4.7.0
+
+ :func:`is_typeddict` now returns ``False`` when called with
+ :data:`TypedDict` itself as the argument, consistent with the
+ behavior of :py:func:`typing.is_typeddict`.
+
.. function:: reveal_type(obj)
See :py:func:`typing.reveal_type`. In ``typing`` since 3.11.
diff --git a/src/_typed_dict_test_helper.py b/src/_typed_dict_test_helper.py
index 7ffc5e1..c5582b1 100644
--- a/src/_typed_dict_test_helper.py
+++ b/src/_typed_dict_test_helper.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from typing import Generic, Optional, T
-from typing_extensions import TypedDict
+from typing_extensions import TypedDict, Annotated, Required
# this class must not be imported into test_typing_extensions.py at top level, otherwise
@@ -16,3 +16,7 @@
class FooGeneric(TypedDict, Generic[T]):
a: Optional[T]
+
+
+class VeryAnnotated(TypedDict, total=False):
+ a: Annotated[Annotated[Annotated[Required[int], "a"], "b"], "c"]
diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py
index 4bec45f..129cdca 100644
--- a/src/test_typing_extensions.py
+++ b/src/test_typing_extensions.py
@@ -38,7 +38,7 @@
from typing_extensions import clear_overloads, get_overloads, overload
from typing_extensions import NamedTuple
from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar
-from _typed_dict_test_helper import Foo, FooGeneric
+from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated
# Flags used to mark tests that only apply after a specific
# version of the typing module.
@@ -1148,10 +1148,26 @@
title: Required[str]
year: int
+class ParentNontotalMovie(TypedDict, total=False):
+ title: Required[str]
+
+class ChildTotalMovie(ParentNontotalMovie):
+ year: NotRequired[int]
+
+class ParentDeeplyAnnotatedMovie(TypedDict):
+ title: Annotated[Annotated[Required[str], "foobar"], "another level"]
+
+class ChildDeeplyAnnotatedMovie(ParentDeeplyAnnotatedMovie):
+ year: NotRequired[Annotated[int, 2000]]
+
class AnnotatedMovie(TypedDict):
title: Annotated[Required[str], "foobar"]
year: NotRequired[Annotated[int, 2000]]
+class WeirdlyQuotedMovie(TypedDict):
+ title: Annotated['Annotated[Required[str], "foobar"]', "another level"]
+ year: NotRequired['Annotated[int, 2000]']
+
gth = get_type_hints
@@ -3036,8 +3052,7 @@
class TypedDictTests(BaseTestCase):
-
- def test_basics_iterable_syntax(self):
+ def test_basics_functional_syntax(self):
Emp = TypedDict('Emp', {'name': str, 'id': int})
self.assertIsSubclass(Emp, dict)
self.assertIsSubclass(Emp, typing.MutableMapping)
@@ -3091,7 +3106,6 @@
self.assertEqual(a['fields'], [('bar', tuple)])
self.assertEqual(a['_fields'], {'baz', set})
- @skipIf(hasattr(typing, 'TypedDict'), "Should be tested by upstream")
def test_typeddict_create_errors(self):
with self.assertRaises(TypeError):
TypedDict.__new__()
@@ -3101,12 +3115,7 @@
TypedDict('Emp', [('name', str)], None)
with self.assertWarns(DeprecationWarning):
- Emp = TypedDict(_typename='Emp', name=str, id=int)
- self.assertEqual(Emp.__name__, 'Emp')
- self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
-
- with self.assertWarns(DeprecationWarning):
- Emp = TypedDict('Emp', _fields={'name': str, 'id': int})
+ Emp = TypedDict('Emp', name=str, id=int)
self.assertEqual(Emp.__name__, 'Emp')
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
@@ -3135,7 +3144,7 @@
def test_py36_class_syntax_usage(self):
self.assertEqual(LabelPoint2D.__name__, 'LabelPoint2D')
self.assertEqual(LabelPoint2D.__module__, __name__)
- self.assertEqual(get_type_hints(LabelPoint2D), {'x': int, 'y': int, 'label': str})
+ self.assertEqual(LabelPoint2D.__annotations__, {'x': int, 'y': int, 'label': str})
self.assertEqual(LabelPoint2D.__bases__, (dict,))
self.assertEqual(LabelPoint2D.__total__, True)
self.assertNotIsSubclass(LabelPoint2D, typing.Sequence)
@@ -3147,11 +3156,9 @@
def test_pickle(self):
global EmpD # pickle wants to reference the class by name
- EmpD = TypedDict('EmpD', {"name": str, "id": int})
+ EmpD = TypedDict('EmpD', {'name': str, 'id': int})
jane = EmpD({'name': 'jane', 'id': 37})
- point = Point2DGeneric(a=5.0, b=3.0)
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
- # Test non-generic TypedDict
z = pickle.dumps(jane, proto)
jane2 = pickle.loads(z)
self.assertEqual(jane2, jane)
@@ -3159,17 +3166,20 @@
ZZ = pickle.dumps(EmpD, proto)
EmpDnew = pickle.loads(ZZ)
self.assertEqual(EmpDnew({'name': 'jane', 'id': 37}), jane)
- # and generic TypedDict
- y = pickle.dumps(point, proto)
- point2 = pickle.loads(y)
- self.assertEqual(point, point2)
+
+ def test_pickle_generic(self):
+ point = Point2DGeneric(a=5.0, b=3.0)
+ for proto in range(pickle.HIGHEST_PROTOCOL + 1):
+ z = pickle.dumps(point, proto)
+ point2 = pickle.loads(z)
+ self.assertEqual(point2, point)
self.assertEqual(point2, {'a': 5.0, 'b': 3.0})
- YY = pickle.dumps(Point2DGeneric, proto)
- Point2DGenericNew = pickle.loads(YY)
+ ZZ = pickle.dumps(Point2DGeneric, proto)
+ Point2DGenericNew = pickle.loads(ZZ)
self.assertEqual(Point2DGenericNew({'a': 5.0, 'b': 3.0}), point)
def test_optional(self):
- EmpD = TypedDict('EmpD', {"name": str, "id": int})
+ EmpD = TypedDict('EmpD', {'name': str, 'id': int})
self.assertEqual(typing.Optional[EmpD], typing.Union[None, EmpD])
self.assertNotEqual(typing.List[EmpD], typing.Tuple[EmpD])
@@ -3189,25 +3199,30 @@
self.assertEqual(Options.__optional_keys__, {'log_level', 'log_path'})
def test_optional_keys(self):
+ class Point2Dor3D(Point2D, total=False):
+ z: int
+
assert Point2Dor3D.__required_keys__ == frozenset(['x', 'y'])
assert Point2Dor3D.__optional_keys__ == frozenset(['z'])
- def test_required_notrequired_keys(self):
- assert NontotalMovie.__required_keys__ == frozenset({'title'})
- assert NontotalMovie.__optional_keys__ == frozenset({'year'})
-
- assert TotalMovie.__required_keys__ == frozenset({'title'})
- assert TotalMovie.__optional_keys__ == frozenset({'year'})
-
-
def test_keys_inheritance(self):
+ class BaseAnimal(TypedDict):
+ name: str
+
+ class Animal(BaseAnimal, total=False):
+ voice: str
+ tail: bool
+
+ class Cat(Animal):
+ fur_color: str
+
assert BaseAnimal.__required_keys__ == frozenset(['name'])
assert BaseAnimal.__optional_keys__ == frozenset([])
- assert get_type_hints(BaseAnimal) == {'name': str}
+ assert BaseAnimal.__annotations__ == {'name': str}
assert Animal.__required_keys__ == frozenset(['name'])
assert Animal.__optional_keys__ == frozenset(['tail', 'voice'])
- assert get_type_hints(Animal) == {
+ assert Animal.__annotations__ == {
'name': str,
'tail': bool,
'voice': str,
@@ -3215,19 +3230,168 @@
assert Cat.__required_keys__ == frozenset(['name', 'fur_color'])
assert Cat.__optional_keys__ == frozenset(['tail', 'voice'])
- assert get_type_hints(Cat) == {
+ assert Cat.__annotations__ == {
'fur_color': str,
'name': str,
'tail': bool,
'voice': str,
}
+ def test_required_notrequired_keys(self):
+ self.assertEqual(NontotalMovie.__required_keys__,
+ frozenset({"title"}))
+ self.assertEqual(NontotalMovie.__optional_keys__,
+ frozenset({"year"}))
+
+ self.assertEqual(TotalMovie.__required_keys__,
+ frozenset({"title"}))
+ self.assertEqual(TotalMovie.__optional_keys__,
+ frozenset({"year"}))
+
+ self.assertEqual(VeryAnnotated.__required_keys__,
+ frozenset())
+ self.assertEqual(VeryAnnotated.__optional_keys__,
+ frozenset({"a"}))
+
+ self.assertEqual(AnnotatedMovie.__required_keys__,
+ frozenset({"title"}))
+ self.assertEqual(AnnotatedMovie.__optional_keys__,
+ frozenset({"year"}))
+
+ self.assertEqual(WeirdlyQuotedMovie.__required_keys__,
+ frozenset({"title"}))
+ self.assertEqual(WeirdlyQuotedMovie.__optional_keys__,
+ frozenset({"year"}))
+
+ self.assertEqual(ChildTotalMovie.__required_keys__,
+ frozenset({"title"}))
+ self.assertEqual(ChildTotalMovie.__optional_keys__,
+ frozenset({"year"}))
+
+ self.assertEqual(ChildDeeplyAnnotatedMovie.__required_keys__,
+ frozenset({"title"}))
+ self.assertEqual(ChildDeeplyAnnotatedMovie.__optional_keys__,
+ frozenset({"year"}))
+
+ def test_multiple_inheritance(self):
+ class One(TypedDict):
+ one: int
+ class Two(TypedDict):
+ two: str
+ class Untotal(TypedDict, total=False):
+ untotal: str
+ Inline = TypedDict('Inline', {'inline': bool})
+ class Regular:
+ pass
+
+ class Child(One, Two):
+ child: bool
+ self.assertEqual(
+ Child.__required_keys__,
+ frozenset(['one', 'two', 'child']),
+ )
+ self.assertEqual(
+ Child.__optional_keys__,
+ frozenset([]),
+ )
+ self.assertEqual(
+ Child.__annotations__,
+ {'one': int, 'two': str, 'child': bool},
+ )
+
+ class ChildWithOptional(One, Untotal):
+ child: bool
+ self.assertEqual(
+ ChildWithOptional.__required_keys__,
+ frozenset(['one', 'child']),
+ )
+ self.assertEqual(
+ ChildWithOptional.__optional_keys__,
+ frozenset(['untotal']),
+ )
+ self.assertEqual(
+ ChildWithOptional.__annotations__,
+ {'one': int, 'untotal': str, 'child': bool},
+ )
+
+ class ChildWithTotalFalse(One, Untotal, total=False):
+ child: bool
+ self.assertEqual(
+ ChildWithTotalFalse.__required_keys__,
+ frozenset(['one']),
+ )
+ self.assertEqual(
+ ChildWithTotalFalse.__optional_keys__,
+ frozenset(['untotal', 'child']),
+ )
+ self.assertEqual(
+ ChildWithTotalFalse.__annotations__,
+ {'one': int, 'untotal': str, 'child': bool},
+ )
+
+ class ChildWithInlineAndOptional(Untotal, Inline):
+ child: bool
+ self.assertEqual(
+ ChildWithInlineAndOptional.__required_keys__,
+ frozenset(['inline', 'child']),
+ )
+ self.assertEqual(
+ ChildWithInlineAndOptional.__optional_keys__,
+ frozenset(['untotal']),
+ )
+ self.assertEqual(
+ ChildWithInlineAndOptional.__annotations__,
+ {'inline': bool, 'untotal': str, 'child': bool},
+ )
+
+ wrong_bases = [
+ (One, Regular),
+ (Regular, One),
+ (One, Two, Regular),
+ (Inline, Regular),
+ (Untotal, Regular),
+ ]
+ for bases in wrong_bases:
+ with self.subTest(bases=bases):
+ with self.assertRaisesRegex(
+ TypeError,
+ 'cannot inherit from both a TypedDict type and a non-TypedDict',
+ ):
+ class Wrong(*bases):
+ pass
+
def test_is_typeddict(self):
- assert is_typeddict(Point2D) is True
- assert is_typeddict(Point2Dor3D) is True
- assert is_typeddict(Union[str, int]) is False
+ self.assertIs(is_typeddict(Point2D), True)
+ self.assertIs(is_typeddict(Point2Dor3D), True)
+ self.assertIs(is_typeddict(Union[str, int]), False)
# classes, not instances
- assert is_typeddict(Point2D()) is False
+ self.assertIs(is_typeddict(Point2D()), False)
+ call_based = TypedDict('call_based', {'a': int})
+ self.assertIs(is_typeddict(call_based), True)
+ self.assertIs(is_typeddict(call_based()), False)
+
+ T = TypeVar("T")
+ class BarGeneric(TypedDict, Generic[T]):
+ a: T
+ self.assertIs(is_typeddict(BarGeneric), True)
+ self.assertIs(is_typeddict(BarGeneric[int]), False)
+ self.assertIs(is_typeddict(BarGeneric()), False)
+
+ if hasattr(typing, "TypeAliasType"):
+ ns = {"TypedDict": TypedDict}
+ exec("""if True:
+ class NewGeneric[T](TypedDict):
+ a: T
+ """, ns)
+ NewGeneric = ns["NewGeneric"]
+ self.assertIs(is_typeddict(NewGeneric), True)
+ self.assertIs(is_typeddict(NewGeneric[int]), False)
+ self.assertIs(is_typeddict(NewGeneric()), False)
+
+ # The TypedDict constructor is not itself a TypedDict
+ self.assertIs(is_typeddict(TypedDict), False)
+ if hasattr(typing, "TypedDict"):
+ self.assertIs(is_typeddict(typing.TypedDict), False)
@skipUnless(TYPING_3_8_0, "Python 3.8+ required")
def test_is_typeddict_against_typeddict_from_typing(self):
@@ -3266,6 +3430,24 @@
{'a': typing.Optional[T], 'b': int, 'c': str}
)
+ @skipUnless(TYPING_3_12_0, "PEP 695 required")
+ def test_pep695_generic_typeddict(self):
+ ns = {"TypedDict": TypedDict}
+ exec("""if True:
+ class A[T](TypedDict):
+ a: T
+ """, ns)
+ A = ns["A"]
+ T, = A.__type_params__
+ self.assertIsInstance(T, TypeVar)
+ self.assertEqual(T.__name__, 'T')
+ self.assertEqual(A.__bases__, (Generic, dict))
+ self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T]))
+ self.assertEqual(A.__mro__, (A, Generic, dict, object))
+ self.assertEqual(A.__parameters__, (T,))
+ self.assertEqual(A[str].__parameters__, ())
+ self.assertEqual(A[str].__args__, (str,))
+
def test_generic_inheritance(self):
class A(TypedDict, Generic[T]):
a: T
@@ -3331,11 +3513,11 @@
self.assertEqual(Point3D.__total__, True)
self.assertEqual(Point3D.__optional_keys__, frozenset())
self.assertEqual(Point3D.__required_keys__, frozenset(['a', 'b', 'c']))
- assert Point3D.__annotations__ == {
+ self.assertEqual(Point3D.__annotations__, {
'a': T,
'b': T,
'c': KT,
- }
+ })
self.assertEqual(Point3D[int, str].__origin__, Point3D)
with self.assertRaises(TypeError):
@@ -3370,6 +3552,64 @@
with self.assertRaises(TypeError):
WithImplicitAny[str]
+ @skipUnless(TYPING_3_9_0, "Was changed in 3.9")
+ def test_non_generic_subscript(self):
+ # For backward compatibility, subscription works
+ # on arbitrary TypedDict types.
+ # (But we don't attempt to backport this misfeature onto 3.7 and 3.8.)
+ class TD(TypedDict):
+ a: T
+ A = TD[int]
+ self.assertEqual(A.__origin__, TD)
+ self.assertEqual(A.__parameters__, ())
+ self.assertEqual(A.__args__, (int,))
+ a = A(a=1)
+ self.assertIs(type(a), dict)
+ self.assertEqual(a, {'a': 1})
+
+ def test_orig_bases(self):
+ T = TypeVar('T')
+
+ class Parent(TypedDict):
+ pass
+
+ class Child(Parent):
+ pass
+
+ class OtherChild(Parent):
+ pass
+
+ class MixedChild(Child, OtherChild, Parent):
+ pass
+
+ class GenericParent(TypedDict, Generic[T]):
+ pass
+
+ class GenericChild(GenericParent[int]):
+ pass
+
+ class OtherGenericChild(GenericParent[str]):
+ pass
+
+ class MixedGenericChild(GenericChild, OtherGenericChild, GenericParent[float]):
+ pass
+
+ class MultipleGenericBases(GenericParent[int], GenericParent[float]):
+ pass
+
+ CallTypedDict = TypedDict('CallTypedDict', {})
+
+ self.assertEqual(Parent.__orig_bases__, (TypedDict,))
+ self.assertEqual(Child.__orig_bases__, (Parent,))
+ self.assertEqual(OtherChild.__orig_bases__, (Parent,))
+ self.assertEqual(MixedChild.__orig_bases__, (Child, OtherChild, Parent,))
+ self.assertEqual(GenericParent.__orig_bases__, (TypedDict, Generic[T]))
+ self.assertEqual(GenericChild.__orig_bases__, (GenericParent[int],))
+ self.assertEqual(OtherGenericChild.__orig_bases__, (GenericParent[str],))
+ self.assertEqual(MixedGenericChild.__orig_bases__, (GenericChild, OtherGenericChild, GenericParent[float]))
+ self.assertEqual(MultipleGenericBases.__orig_bases__, (GenericParent[int], GenericParent[float]))
+ self.assertEqual(CallTypedDict.__orig_bases__, (TypedDict,))
+
class AnnotatedTests(BaseTestCase):
diff --git a/src/typing_extensions.py b/src/typing_extensions.py
index 9e33402..e8ce9ee 100644
--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -918,113 +918,56 @@
_TypedDictMeta = typing._TypedDictMeta
is_typeddict = typing.is_typeddict
else:
- def _check_fails(cls, other):
- try:
- if _caller() not in {'abc', 'functools', 'typing'}:
- # Typed dicts are only for static structural subtyping.
- raise TypeError('TypedDict does not support instance and class checks')
- except (AttributeError, ValueError):
- pass
- return False
-
- def _dict_new(*args, **kwargs):
- if not args:
- raise TypeError('TypedDict.__new__(): not enough arguments')
- _, args = args[0], args[1:] # allow the "cls" keyword be passed
- return dict(*args, **kwargs)
-
- _dict_new.__text_signature__ = '($cls, _typename, _fields=None, /, **kwargs)'
-
- def _typeddict_new(*args, total=True, **kwargs):
- if not args:
- raise TypeError('TypedDict.__new__(): not enough arguments')
- _, args = args[0], args[1:] # allow the "cls" keyword be passed
- if args:
- typename, args = args[0], args[1:] # allow the "_typename" keyword be passed
- elif '_typename' in kwargs:
- typename = kwargs.pop('_typename')
- warnings.warn("Passing '_typename' as keyword argument is deprecated",
- DeprecationWarning, stacklevel=2)
- else:
- raise TypeError("TypedDict.__new__() missing 1 required positional "
- "argument: '_typename'")
- if args:
- try:
- fields, = args # allow the "_fields" keyword be passed
- except ValueError:
- raise TypeError('TypedDict.__new__() takes from 2 to 3 '
- f'positional arguments but {len(args) + 2} '
- 'were given')
- elif '_fields' in kwargs and len(kwargs) == 1:
- fields = kwargs.pop('_fields')
- warnings.warn("Passing '_fields' as keyword argument is deprecated",
- DeprecationWarning, stacklevel=2)
- else:
- fields = None
-
- if fields is None:
- fields = kwargs
- elif kwargs:
- raise TypeError("TypedDict takes either a dict or keyword arguments,"
- " but not both")
-
- if kwargs:
- warnings.warn(
- "The kwargs-based syntax for TypedDict definitions is deprecated, "
- "may be removed in a future version, and may not be "
- "understood by third-party type checkers.",
- DeprecationWarning,
- stacklevel=2,
- )
-
- ns = {'__annotations__': dict(fields)}
- module = _caller()
- if module is not None:
- # Setting correct module is necessary to make typed dict classes pickleable.
- ns['__module__'] = module
-
- return _TypedDictMeta(typename, (), ns, total=total)
-
- _typeddict_new.__text_signature__ = ('($cls, _typename, _fields=None,'
- ' /, *, total=True, **kwargs)')
-
+ # 3.10.0 and later
_TAKES_MODULE = "module" in inspect.signature(typing._type_check).parameters
+ if sys.version_info >= (3, 8):
+ _fake_name = "Protocol"
+ else:
+ _fake_name = "_Protocol"
+
class _TypedDictMeta(type):
- def __init__(cls, name, bases, ns, total=True):
- super().__init__(name, bases, ns)
-
def __new__(cls, name, bases, ns, total=True):
- # Create new typed dict class object.
- # This method is called directly when TypedDict is subclassed,
- # or via _typeddict_new when TypedDict is instantiated. This way
- # TypedDict supports all three syntaxes described in its docstring.
- # Subclasses and instances of TypedDict return actual dictionaries
- # via _dict_new.
- ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new
- # Don't insert typing.Generic into __bases__ here,
- # or Generic.__init_subclass__ will raise TypeError
- # in the super().__new__() call.
- # Instead, monkey-patch __bases__ onto the class after it's been created.
- tp_dict = super().__new__(cls, name, (dict,), ns)
+ """Create new typed dict class object.
- is_generic = any(issubclass(base, typing.Generic) for base in bases)
+ This method is called when TypedDict is subclassed,
+ or when TypedDict is instantiated. This way
+ TypedDict supports all three syntax forms described in its docstring.
+ Subclasses and instances of TypedDict return actual dictionaries.
+ """
+ for base in bases:
+ if type(base) is not _TypedDictMeta and base is not typing.Generic:
+ raise TypeError('cannot inherit from both a TypedDict type '
+ 'and a non-TypedDict base class')
- if is_generic:
- tp_dict.__bases__ = (typing.Generic, dict)
- _maybe_adjust_parameters(tp_dict)
+ if any(issubclass(b, typing.Generic) for b in bases):
+ generic_base = (typing.Generic,)
else:
- # generic TypedDicts get __orig_bases__ from Generic
- tp_dict.__orig_bases__ = bases or (TypedDict,)
+ generic_base = ()
+
+ # typing.py generally doesn't let you inherit from plain Generic, unless
+ # the name of the class happens to be "Protocol" (or "_Protocol" on 3.7).
+ tp_dict = type.__new__(_TypedDictMeta, _fake_name, (*generic_base, dict), ns)
+ tp_dict.__name__ = name
+ if tp_dict.__qualname__ == _fake_name:
+ tp_dict.__qualname__ = name
+
+ if not hasattr(tp_dict, '__orig_bases__'):
+ tp_dict.__orig_bases__ = bases
annotations = {}
own_annotations = ns.get('__annotations__', {})
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
- kwds = {"module": tp_dict.__module__} if _TAKES_MODULE else {}
- own_annotations = {
- n: typing._type_check(tp, msg, **kwds)
- for n, tp in own_annotations.items()
- }
+ if _TAKES_MODULE:
+ own_annotations = {
+ n: typing._type_check(tp, msg, module=tp_dict.__module__)
+ for n, tp in own_annotations.items()
+ }
+ else:
+ own_annotations = {
+ n: typing._type_check(tp, msg)
+ for n, tp in own_annotations.items()
+ }
required_keys = set()
optional_keys = set()
@@ -1058,15 +1001,19 @@
tp_dict.__total__ = total
return tp_dict
- __instancecheck__ = __subclasscheck__ = _check_fails
+ __call__ = dict # static method
- TypedDict = _TypedDictMeta('TypedDict', (dict,), {})
- TypedDict.__module__ = __name__
- TypedDict.__doc__ = \
- """A simple typed name space. At runtime it is equivalent to a plain dict.
+ def __subclasscheck__(cls, other):
+ # Typed dicts are only for static structural subtyping.
+ raise TypeError('TypedDict does not support instance and class checks')
+
+ __instancecheck__ = __subclasscheck__
+
+ def TypedDict(__typename, __fields=None, *, total=True, **kwargs):
+ """A simple typed namespace. At runtime it is equivalent to a plain dict.
TypedDict creates a dictionary type that expects all of its
- instances to have a certain set of keys, with each key
+ instances to have a certain set of keys, where each key is
associated with a value of a consistent type. This expectation
is not checked at runtime but is only enforced by type checkers.
Usage::
@@ -1083,14 +1030,49 @@
The type info can be accessed via the Point2D.__annotations__ dict, and
the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets.
- TypedDict supports two additional equivalent forms::
+ TypedDict supports an additional equivalent form::
- Point2D = TypedDict('Point2D', x=int, y=int, label=str)
Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str})
- The class syntax is only supported in Python 3.6+, while two other
- syntax forms work for Python 2.7 and 3.2+
+ By default, all keys must be present in a TypedDict. It is possible
+ to override this by specifying totality.
+ Usage::
+
+ class point2D(TypedDict, total=False):
+ x: int
+ y: int
+
+ This means that a point2D TypedDict can have any of the keys omitted. A type
+ checker is only expected to support a literal False or True as the value of
+ the total argument. True is the default, and makes all items defined in the
+ class body be required.
"""
+ if __fields is None:
+ __fields = kwargs
+ elif kwargs:
+ raise TypeError("TypedDict takes either a dict or keyword arguments,"
+ " but not both")
+ if kwargs:
+ warnings.warn(
+ "The kwargs-based syntax for TypedDict definitions is deprecated "
+ "in Python 3.11, will be removed in Python 3.13, and may not be "
+ "understood by third-party type checkers.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+ ns = {'__annotations__': dict(__fields)}
+ module = _caller()
+ if module is not None:
+ # Setting correct module is necessary to make typed dict classes pickleable.
+ ns['__module__'] = module
+
+ td = _TypedDictMeta(__typename, (), ns, total=total)
+ td.__orig_bases__ = (TypedDict,)
+ return td
+
+ _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {})
+ TypedDict.__mro_entries__ = lambda bases: (_TypedDict,)
if hasattr(typing, "_TypedDictMeta"):
_TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta)
@@ -1108,7 +1090,10 @@
is_typeddict(Film) # => True
is_typeddict(Union[list, str]) # => False
"""
- return isinstance(tp, tuple(_TYPEDDICT_TYPES))
+ # On 3.8, this would otherwise return True
+ if hasattr(typing, "TypedDict") and tp is typing.TypedDict:
+ return False
+ return isinstance(tp, _TYPEDDICT_TYPES)
if hasattr(typing, "assert_type"):