https://github.com/python/cpython/commit/d6cb8fa86eb3c6e41e532c247a02c1c3df93e744
commit: d6cb8fa86eb3c6e41e532c247a02c1c3df93e744
branch: 3.14
author: Miss Islington (bot) <31488909+miss-isling...@users.noreply.github.com>
committer: JelleZijlstra <jelle.zijls...@gmail.com>
date: 2025-05-14T13:54:29Z
summary:

[3.14] gh-133701: Fix incorrect `__annotations__` on TypedDict defined under 
PEP 563 (GH-133772) (#134003)

gh-133701: Fix incorrect `__annotations__` on TypedDict defined under PEP 563 
(GH-133772)
(cherry picked from commit 9836503b48e047db117b3bef3a812c40ed3e988a)

Co-authored-by: Jelle Zijlstra <jelle.zijls...@gmail.com>

files:
A Misc/NEWS.d/next/Library/2025-05-09-08-49-03.gh-issue-133701.KI8tGz.rst
M Lib/test/support/__init__.py
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index c74c3a3190947b..9b6e80fdad9747 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -696,9 +696,11 @@ def sortdict(dict):
     return "{%s}" % withcommas
 
 
-def run_code(code: str) -> dict[str, object]:
+def run_code(code: str, extra_names: dict[str, object] | None = None) -> 
dict[str, object]:
     """Run a piece of code after dedenting it, and return its global 
namespace."""
     ns = {}
+    if extra_names:
+        ns.update(extra_names)
     exec(textwrap.dedent(code), ns)
     return ns
 
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index a4fab1d5e14805..b34bfcc90b1577 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -8538,6 +8538,36 @@ class Child(Base1, Base2):
         self.assertEqual(Child.__required_keys__, frozenset(['a']))
         self.assertEqual(Child.__optional_keys__, frozenset())
 
+    def test_inheritance_pep563(self):
+        def _make_td(future, class_name, annos, base, extra_names=None):
+            lines = []
+            if future:
+                lines.append('from __future__ import annotations')
+            lines.append('from typing import TypedDict')
+            lines.append(f'class {class_name}({base}):')
+            for name, anno in annos.items():
+                lines.append(f'    {name}: {anno}')
+            code = '\n'.join(lines)
+            ns = run_code(code, extra_names)
+            return ns[class_name]
+
+        for base_future in (True, False):
+            for child_future in (True, False):
+                with self.subTest(base_future=base_future, 
child_future=child_future):
+                    base = _make_td(
+                        base_future, "Base", {"base": "int"}, "TypedDict"
+                    )
+                    self.assertIsNotNone(base.__annotate__)
+                    child = _make_td(
+                        child_future, "Child", {"child": "int"}, "Base", 
{"Base": base}
+                    )
+                    base_anno = ForwardRef("int", module="builtins") if 
base_future else int
+                    child_anno = ForwardRef("int", module="builtins") if 
child_future else int
+                    self.assertEqual(base.__annotations__, {'base': base_anno})
+                    self.assertEqual(
+                        child.__annotations__, {'child': child_anno, 'base': 
base_anno}
+                    )
+
     def test_required_notrequired_keys(self):
         self.assertEqual(NontotalMovie.__required_keys__,
                          frozenset({"title"}))
diff --git a/Lib/typing.py b/Lib/typing.py
index 0695a825283c7a..30d53cb46f8a18 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -3087,14 +3087,16 @@ def __new__(cls, name, bases, ns, total=True):
         else:
             generic_base = ()
 
+        ns_annotations = ns.pop('__annotations__', None)
+
         tp_dict = type.__new__(_TypedDictMeta, name, (*generic_base, dict), ns)
 
         if not hasattr(tp_dict, '__orig_bases__'):
             tp_dict.__orig_bases__ = bases
 
-        if "__annotations__" in ns:
+        if ns_annotations is not None:
             own_annotate = None
-            own_annotations = ns["__annotations__"]
+            own_annotations = ns_annotations
         elif (own_annotate := 
_lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None:
             own_annotations = _lazy_annotationlib.call_annotate_function(
                 own_annotate, _lazy_annotationlib.Format.FORWARDREF, 
owner=tp_dict
@@ -3165,7 +3167,7 @@ def __annotate__(format):
                 if base_annotate is None:
                     continue
                 base_annos = _lazy_annotationlib.call_annotate_function(
-                    base.__annotate__, format, owner=base)
+                    base_annotate, format, owner=base)
                 annos.update(base_annos)
             if own_annotate is not None:
                 own = _lazy_annotationlib.call_annotate_function(
diff --git 
a/Misc/NEWS.d/next/Library/2025-05-09-08-49-03.gh-issue-133701.KI8tGz.rst 
b/Misc/NEWS.d/next/Library/2025-05-09-08-49-03.gh-issue-133701.KI8tGz.rst
new file mode 100644
index 00000000000000..163d9b331d1f8a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-05-09-08-49-03.gh-issue-133701.KI8tGz.rst
@@ -0,0 +1,3 @@
+Fix bug where :class:`typing.TypedDict` classes defined under ``from
+__future__ import annotations`` and inheriting from another ``TypedDict``
+had an incorrect ``__annotations__`` attribute.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to