Skip to content

Commit aec3bc9

Browse files
committed
Drop support for mypy_extensions.TypedDict
Resolves #23
1 parent 7e41218 commit aec3bc9

File tree

6 files changed

+13
-118
lines changed

6 files changed

+13
-118
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Single-file implementation in `trycast/__init__.py` (~1300 lines).
2222
else:
2323
from typing import _eval_type as eval_type
2424
```
25-
- TypedDict compatibility: prefer `typing.TypedDict`, fallback to `typing_extensions.TypedDict`, avoid `mypy_extensions.TypedDict`
25+
- TypedDict compatibility: prefer `typing.TypedDict`, fallback to `typing_extensions.TypedDict`
2626

2727
### Debugging CPython typing.py Changes
2828

README.md

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,6 @@ python -m pip install trycast
219219
not-required keys correctly:
220220
* Use Python 3.9+.
221221
* Prefer using `typing.TypedDict` instead of `typing_extensions.TypedDict`.
222-
* Avoid using `mypy_extensions.TypedDict` in general.
223222
224223
225224
## Presentations & Videos
@@ -307,7 +306,6 @@ make help
307306
* TypedDict
308307
* typing.TypedDict, typing_extensions.TypedDict
309308
([PEP 589](https://peps.python.org/pep-0589/))
310-
* mypy_extensions.TypedDict (when strict=False)
311309
* –––
312310
* Required, NotRequired
313311
([PEP 655](https://peps.python.org/pep-0655/))
@@ -399,12 +397,6 @@ also be a valid complex value, as consistent with Python typecheckers:
399397
Parameters:
400398
401399
* **strict** --
402-
* If strict=False then this function will additionally accept
403-
mypy_extensions.TypedDict instances for the `tp` parameter.
404-
Normally these kinds of types are
405-
rejected with a TypeNotSupportedError because these
406-
types do not preserve enough information at runtime to reliably
407-
determine which keys are required and which are potentially-missing.
408400
* If strict=False then `NewType("Foo", T)` will be treated
409401
the same as `T`. Normally NewTypes are rejected with a
410402
TypeNotSupportedError because values of NewTypes at runtime
@@ -417,7 +409,6 @@ Parameters:
417409
Raises:
418410
419411
* **TypeNotSupportedError** --
420-
* If strict=True and mypy_extensions.TypedDict is found within the `tp` argument.
421412
* If strict=True and a NewType is found within the `tp` argument.
422413
* If a TypeVar is found within the `tp` argument.
423414
* If an unrecognized Generic type is found within the `tp` argument.
@@ -524,9 +515,12 @@ raised exceptions, and other details.
524515
### main (v2.0.0)
525516
526517
* Drop support for Python 3.8.
527-
* Remove supporting code for Python 3.8's neutered TypedDict.
518+
* Drop support for Python 3.8's neutered TypedDict,
519+
which lacks metadata present in later versions.
528520
* Drop support for Pytype type checker, since it has been
529-
[deprecated](https://github.com/google/pytype/blob/main/README.md).
521+
[deprecated](https://github.com/google/pytype/blob/main/README.md)
522+
as of Aug 2025.
523+
* Drop support for mypy_extensions.TypedDict, since it was deprecated in Aug 2023.
530524
531525
### v1.2.1
532526

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ python = ">=3.9"
3333

3434
[tool.poetry.group.dev.dependencies]
3535
mypy = "*"
36-
mypy_extensions = "*"
3736
pyright = "*"
3837
pyre-check = "*"
3938
tox = ">=4.0.0,<5.0.0"

tests.py

Lines changed: 4 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434
from typing import TypeVar, Union, cast
3535
from unittest import SkipTest, TestCase
3636

37-
import mypy_extensions
38-
3937
import test_data.forwardrefs_example
4038
import test_data.forwardrefs_example_with_import_annotations
4139
from tests_shape_example import HTTP_400_BAD_REQUEST, draw_shape_endpoint, shapes_drawn
@@ -1853,24 +1851,8 @@ def test_types_defined_in_module_with_import_annotations(self) -> None:
18531851
)
18541852

18551853
# === strict=True mode ===
1856-
1857-
def test_rejects_mypy_typeddict_when_strict_is_true(self) -> None:
1858-
class Point2D(mypy_extensions.TypedDict): # type: ignore[reportGeneralTypeIssues] # pyright
1859-
x: int
1860-
y: int
1861-
1862-
class Point3D(Point2D, total=False): # type: ignore[reportGeneralTypeIssues] # pyright
1863-
z: int
1864-
1865-
self.assertRaisesRegex(
1866-
TypeNotSupportedError,
1867-
(
1868-
"trycast cannot determine which keys are required.*?"
1869-
"Suggest use a typing(_extensions)?.TypedDict.*?"
1870-
"strict=False"
1871-
),
1872-
lambda: trycast(Point3D, {"x": 1, "y": 2}, strict=True),
1873-
)
1854+
1855+
# (TODO: Add test related to NewTypes)
18741856

18751857
# NOTE: Cannot combine the following two if-checks with an `and`
18761858
# because that is too complicated for Pyre to understand.
@@ -1899,48 +1881,8 @@ class Point3D(Point2D, total=False):
18991881
)
19001882

19011883
# === strict=False mode ===
1902-
1903-
def test_accepts_mypy_typeddict_when_strict_is_false(self) -> None:
1904-
class Point2D(mypy_extensions.TypedDict): # type: ignore[reportGeneralTypeIssues] # pyright
1905-
x: int
1906-
y: int
1907-
1908-
# NOTE: mypy_extensions.TypedDict doesn't preserve at runtime
1909-
# which of Point3D's inherited fields from Point2D are required.
1910-
# So trycast() guesses (incorrectly) that ALL fields of Point3D
1911-
# are not-required because Point3D is declared as total=False.
1912-
class Point3D(Point2D, total=False): # type: ignore[reportGeneralTypeIssues] # pyright
1913-
z: int
1914-
1915-
class MaybePoint1D(mypy_extensions.TypedDict, total=False): # type: ignore[reportGeneralTypeIssues] # pyright
1916-
x: int
1917-
1918-
class TaggedMaybePoint1D(MaybePoint1D):
1919-
name: str
1920-
1921-
self.assertTryCastSuccess(Point2D, {"x": 1, "y": 2}, strict=False)
1922-
self.assertTryCastFailure(Point2D, {"x": 1, "y": "string"}, strict=False)
1923-
self.assertTryCastFailure(Point2D, {"x": 1}, strict=False)
1924-
1925-
self.assertTryCastSuccess(Point3D, {"x": 1, "y": 2, "z": 3}, strict=False)
1926-
self.assertTryCastFailure(
1927-
Point3D, {"x": 1, "y": 2, "z": "string"}, strict=False
1928-
)
1929-
self.assertTryCastSuccess(Point3D, {"x": 1, "y": 2}, strict=False)
1930-
self.assertTryCastSuccess(Point3D, {"x": 1}, strict=False) # surprise!
1931-
self.assertTryCastSuccess(Point3D, {"q": 1}, strict=False) # surprise!
1932-
1933-
self.assertTryCastSuccess(MaybePoint1D, {"x": 1}, strict=False)
1934-
self.assertTryCastFailure(MaybePoint1D, {"x": "string"}, strict=False)
1935-
self.assertTryCastSuccess(MaybePoint1D, {}, strict=False)
1936-
self.assertTryCastSuccess(MaybePoint1D, {"q": 1}, strict=False) # surprise!
1937-
1938-
self.assertTryCastSuccess(
1939-
TaggedMaybePoint1D, {"x": 1, "name": "one"}, strict=False
1940-
)
1941-
self.assertTryCastFailure(
1942-
TaggedMaybePoint1D, {"name": "one"}, strict=False
1943-
) # surprise!
1884+
1885+
# (TODO: Add test related to NewTypes)
19441886

19451887
# === Misuse: Nice Error Messages ===
19461888

@@ -2905,11 +2847,6 @@ class TypingExtensionsPoint(TypingExtensionsTypedDict):
29052847
y: int
29062848

29072849

2908-
class MypyExtensionsPoint(mypy_extensions.TypedDict): # type: ignore[reportGeneralTypeIssues] # pyright
2909-
x: int
2910-
y: int
2911-
2912-
29132850
class TestIsTypedDict(TestCase):
29142851
"""
29152852
Tests whether the _is_typed_dict() internal function works.
@@ -2921,9 +2858,6 @@ def test_recognizes_typed_dict_from_typing(self) -> None:
29212858
def test_recognizes_typed_dict_from_typing_extensions(self) -> None:
29222859
self.assertTrue(_is_typed_dict(TypingExtensionsPoint))
29232860

2924-
def test_recognizes_typed_dict_from_mypy_extensions(self) -> None:
2925-
self.assertTrue(_is_typed_dict(MypyExtensionsPoint))
2926-
29272861

29282862
# ------------------------------------------------------------------------------
29292863
# Meta: TestTypechecks

trycast/__init__.py

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,6 @@ def __class_getitem__(cls, key):
138138
except ImportError:
139139
pass
140140

141-
try:
142-
from mypy_extensions import ( # type: ignore[attr-defined] # isort: skip
143-
_TypedDictMeta as _MypyExtensionsTypedDictMeta, # type: ignore[reportGeneralTypeIssues] # pyright
144-
)
145-
146-
_typed_dict_meta_list.append(_MypyExtensionsTypedDictMeta) # type: ignore[16] # pyre
147-
except ImportError:
148-
pass
149-
150141
_typed_dict_metas = tuple(_typed_dict_meta_list)
151142

152143

@@ -364,12 +355,6 @@ def trycast(tp, value, /, failure=None, *, strict=True, eval=True):
364355
365356
Parameters:
366357
* strict --
367-
* If strict=False then this function will additionally accept
368-
mypy_extensions.TypedDict instances for the `tp` parameter.
369-
Normally these kinds of types are
370-
rejected with a TypeNotSupportedError because these
371-
types do not preserve enough information at runtime to reliably
372-
determine which keys are required and which are potentially-missing.
373358
* If strict=False then `NewType("Foo", T)` will be treated
374359
the same as `T`. Normally NewTypes are rejected with a
375360
TypeNotSupportedError because values of NewTypes at runtime
@@ -381,7 +366,6 @@ def trycast(tp, value, /, failure=None, *, strict=True, eval=True):
381366
382367
Raises:
383368
* TypeNotSupportedError --
384-
* If strict=True and mypy_extensions.TypedDict is found within the `tp` argument.
385369
* If strict=True and a NewType is found within the `tp` argument.
386370
* If a TypeVar is found within the `tp` argument.
387371
* If an unrecognized Generic type is found within the `tp` argument.
@@ -768,24 +752,8 @@ def _checkcast_inner(
768752
else:
769753
resolved_annotations = tp.__annotations__
770754

771-
try:
772-
# {typing, typing_extensions}.TypedDict
773-
required_keys = tp.__required_keys__ # type: ignore[attr-defined] # mypy
774-
except AttributeError:
775-
# {mypy_extensions}.TypedDict
776-
if options.strict:
777-
advise = "Suggest use a typing.TypedDict instead."
778-
advise2 = f"Or use {options.funcname}(..., strict=False)."
779-
raise TypeNotSupportedError(
780-
f"{options.funcname} cannot determine which keys are required "
781-
f"and which are potentially-missing for the "
782-
f"specified kind of TypedDict. {advise} {advise2}"
783-
)
784-
else:
785-
if tp.__total__: # type: ignore[attr-defined] # mypy
786-
required_keys = resolved_annotations.keys()
787-
else:
788-
required_keys = frozenset()
755+
# {typing, typing_extensions}.TypedDict
756+
required_keys = tp.__required_keys__ # type: ignore[attr-defined] # mypy
789757

790758
for k, v in value.items():
791759
V = resolved_annotations.get(k, _MISSING)

0 commit comments

Comments
 (0)