Skip to content

Commit d17c456

Browse files
allow TypedDict as a type argument (#614)
1 parent b07d245 commit d17c456

File tree

3 files changed

+63
-43
lines changed

3 files changed

+63
-43
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Unreleased
2+
3+
- Fix usage of `typing_extensions.TypedDict` nested inside other types
4+
(e.g., `typing.Type[typing_extensions.TypedDict]`). This is not allowed by the
5+
type system but worked on older versions, so we maintain support.
6+
17
# Release 4.14.0 (June 2, 2025)
28

39
Changes since 4.14.0rc1:

src/test_typing_extensions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4202,6 +4202,12 @@ def test_basics_functional_syntax(self):
42024202
self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
42034203
self.assertEqual(Emp.__total__, True)
42044204

4205+
def test_allowed_as_type_argument(self):
4206+
# https://github.com/python/typing_extensions/issues/613
4207+
obj = typing.Type[typing_extensions.TypedDict]
4208+
self.assertIs(typing_extensions.get_origin(obj), type)
4209+
self.assertEqual(typing_extensions.get_args(obj), (typing_extensions.TypedDict,))
4210+
42054211
@skipIf(sys.version_info < (3, 13), "Change in behavior in 3.13")
42064212
def test_keywords_syntax_raises_on_3_13(self):
42074213
with self.assertRaises(TypeError), self.assertWarns(DeprecationWarning):

src/typing_extensions.py

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,55 @@ def __new__(cls, *args, **kwargs):
221221

222222
ClassVar = typing.ClassVar
223223

224+
# Vendored from cpython typing._SpecialFrom
225+
# Having a separate class means that instances will not be rejected by
226+
# typing._type_check.
227+
class _SpecialForm(typing._Final, _root=True):
228+
__slots__ = ('_name', '__doc__', '_getitem')
229+
230+
def __init__(self, getitem):
231+
self._getitem = getitem
232+
self._name = getitem.__name__
233+
self.__doc__ = getitem.__doc__
234+
235+
def __getattr__(self, item):
236+
if item in {'__name__', '__qualname__'}:
237+
return self._name
238+
239+
raise AttributeError(item)
240+
241+
def __mro_entries__(self, bases):
242+
raise TypeError(f"Cannot subclass {self!r}")
243+
244+
def __repr__(self):
245+
return f'typing_extensions.{self._name}'
246+
247+
def __reduce__(self):
248+
return self._name
249+
250+
def __call__(self, *args, **kwds):
251+
raise TypeError(f"Cannot instantiate {self!r}")
252+
253+
def __or__(self, other):
254+
return typing.Union[self, other]
255+
256+
def __ror__(self, other):
257+
return typing.Union[other, self]
258+
259+
def __instancecheck__(self, obj):
260+
raise TypeError(f"{self} cannot be used with isinstance()")
261+
262+
def __subclasscheck__(self, cls):
263+
raise TypeError(f"{self} cannot be used with issubclass()")
264+
265+
@typing._tp_cache
266+
def __getitem__(self, parameters):
267+
return self._getitem(self, parameters)
268+
224269

270+
# Note that inheriting from this class means that the object will be
271+
# rejected by typing._type_check, so do not use it if the special form
272+
# is arguably valid as a type by itself.
225273
class _ExtensionsSpecialForm(typing._SpecialForm, _root=True):
226274
def __repr__(self):
227275
return 'typing_extensions.' + self._name
@@ -1223,7 +1271,9 @@ def _create_typeddict(
12231271
td.__orig_bases__ = (TypedDict,)
12241272
return td
12251273

1226-
class _TypedDictSpecialForm(_ExtensionsSpecialForm, _root=True):
1274+
class _TypedDictSpecialForm(_SpecialForm, _root=True):
1275+
__slots__ = ('_name', '__doc__', '_getitem')
1276+
12271277
def __call__(
12281278
self,
12291279
typename,
@@ -2201,48 +2251,6 @@ def cast[T](typ: TypeForm[T], value: Any) -> T: ...
22012251
return typing._GenericAlias(self, (item,))
22022252

22032253

2204-
# Vendored from cpython typing._SpecialFrom
2205-
class _SpecialForm(typing._Final, _root=True):
2206-
__slots__ = ('_name', '__doc__', '_getitem')
2207-
2208-
def __init__(self, getitem):
2209-
self._getitem = getitem
2210-
self._name = getitem.__name__
2211-
self.__doc__ = getitem.__doc__
2212-
2213-
def __getattr__(self, item):
2214-
if item in {'__name__', '__qualname__'}:
2215-
return self._name
2216-
2217-
raise AttributeError(item)
2218-
2219-
def __mro_entries__(self, bases):
2220-
raise TypeError(f"Cannot subclass {self!r}")
2221-
2222-
def __repr__(self):
2223-
return f'typing_extensions.{self._name}'
2224-
2225-
def __reduce__(self):
2226-
return self._name
2227-
2228-
def __call__(self, *args, **kwds):
2229-
raise TypeError(f"Cannot instantiate {self!r}")
2230-
2231-
def __or__(self, other):
2232-
return typing.Union[self, other]
2233-
2234-
def __ror__(self, other):
2235-
return typing.Union[other, self]
2236-
2237-
def __instancecheck__(self, obj):
2238-
raise TypeError(f"{self} cannot be used with isinstance()")
2239-
2240-
def __subclasscheck__(self, cls):
2241-
raise TypeError(f"{self} cannot be used with issubclass()")
2242-
2243-
@typing._tp_cache
2244-
def __getitem__(self, parameters):
2245-
return self._getitem(self, parameters)
22462254

22472255

22482256
if hasattr(typing, "LiteralString"): # 3.11+

0 commit comments

Comments
 (0)