From ad144bf6be1e936f9e88ce0f5ef74fe5eb96fb8f Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke Date: Thu, 11 Sep 2025 20:37:04 +0200 Subject: [PATCH 1/2] Fix TypedDict qualifier inheritance with same name --- src/test_typing_extensions.py | 41 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 22 ++++++++++++++----- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 88fa699e..5930cdef 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4638,6 +4638,47 @@ class ChildWithInlineAndOptional(Untotal, Inline): class Wrong(*bases): pass + def test_keys_inheritance_with_same_name(self): + class NotTotal(TypedDict, total=False): + a: int + + class Total(NotTotal): + a: int + + self.assertEqual(NotTotal.__required_keys__, frozenset()) + self.assertEqual(NotTotal.__optional_keys__, frozenset(['a'])) + self.assertEqual(Total.__required_keys__, frozenset(['a'])) + self.assertEqual(Total.__optional_keys__, frozenset()) + + class Base(TypedDict): + a: NotRequired[int] + b: Required[int] + + class Child(Base): + a: Required[int] + b: NotRequired[int] + + self.assertEqual(Base.__required_keys__, frozenset(['b'])) + self.assertEqual(Base.__optional_keys__, frozenset(['a'])) + self.assertEqual(Child.__required_keys__, frozenset(['a'])) + self.assertEqual(Child.__optional_keys__, frozenset(['b'])) + + def test_multiple_inheritance_with_same_key(self): + class Base1(TypedDict): + a: NotRequired[int] + + class Base2(TypedDict): + a: Required[str] + + class Child(Base1, Base2): + pass + + # Last base wins + self.assertEqual(Child.__annotations__, {'a': Required[str]}) + self.assertEqual(Child.__required_keys__, frozenset(['a'])) + self.assertEqual(Child.__optional_keys__, frozenset()) + + def test_closed_values(self): class Implicit(TypedDict): ... class ExplicitTrue(TypedDict, closed=True): ... diff --git a/src/typing_extensions.py b/src/typing_extensions.py index bd67a80a..0a0dd88f 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1182,8 +1182,14 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None, if sys.version_info <= (3, 14): annotations.update(base_dict.get('__annotations__', {})) - required_keys.update(base_dict.get('__required_keys__', ())) - optional_keys.update(base_dict.get('__optional_keys__', ())) + base_required = base_dict.get('__required_keys__', set()) + required_keys |= base_required + optional_keys -= base_required + + base_optional = base_dict.get('__optional_keys__', set()) + required_keys -= base_optional + optional_keys |= base_optional + readonly_keys.update(base_dict.get('__readonly_keys__', ())) mutable_keys.update(base_dict.get('__mutable_keys__', ())) @@ -1211,13 +1217,19 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None, qualifiers = set(_get_typeddict_qualifiers(annotation_type)) if Required in qualifiers: - required_keys.add(annotation_key) + is_required = True elif NotRequired in qualifiers: - optional_keys.add(annotation_key) - elif total: + is_required = False + else: + is_required = total + + if is_required: required_keys.add(annotation_key) + optional_keys.discard(annotation_key) else: optional_keys.add(annotation_key) + required_keys.discard(annotation_key) + if ReadOnly in qualifiers: mutable_keys.discard(annotation_key) readonly_keys.add(annotation_key) From bbf9dffab5669447c6d8261c4e6b988281197b55 Mon Sep 17 00:00:00 2001 From: Jan-Eric Nitschke <47750513+JanEricNitschke@users.noreply.github.com> Date: Thu, 11 Sep 2025 20:45:13 +0200 Subject: [PATCH 2/2] Add CHANGELOG entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 733505a5..f35c4827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Fix setting of `__required_keys__` and `__optional_keys__` when inheriting + keys with the same name. - Fix incorrect behaviour on Python 3.9 and Python 3.10 that meant that calling `isinstance` with `typing_extensions.Concatenate[...]` or `typing_extensions.Unpack[...]` as the first argument could have a different