From fcab88ab4ebdf8ab39407ecc2769b3a0c0bebf24 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Tue, 6 Oct 2026 22:19:55 -0400 Subject: [PATCH] gh-158936: Resolve lazy imports consistently for dataclass annotations (GH-158939) With string annotations, dataclasses looks up the first name of each annotation to detect ClassVar, InitVar and KW_ONLY. If that name was a lazy import that could not be resolved, the lookup raised and the class could not be created, while the same class works with non-string annotations. This resolves lazy imports the way annotationlib does: try to resolve them, and treat a failure like a missing name. (cherry picked from commit 0f1d4a79f61d6a2e848b6b9c42f0f5f14033832d) Co-authored-by: Brittany Reynoso --- Lib/dataclasses.py | 6 +++++- Lib/test/test_dataclasses/__init__.py | 5 +++++ Lib/test/test_dataclasses/dataclass_lazy_broken.py | 1 + Lib/test/test_dataclasses/dataclass_lazy_str.py | 12 ++++++++++++ .../2026-10-06-20-33-52.gh-issue-158936.flCm_m.rst | 3 +++ 5 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 Lib/test/test_dataclasses/dataclass_lazy_broken.py create mode 100644 Lib/test/test_dataclasses/dataclass_lazy_str.py create mode 100644 Misc/NEWS.d/next/Library/2026-10-06-20-33-52.gh-issue-158936.flCm_m.rst diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 863e4101f98bd6..ac5ec129b4bc06 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -806,7 +806,11 @@ def _get_type_from_annotation(annotation, cls): path = match[1].split(".") root = sys.modules.get(cls.__module__) for path_item in path: - root = getattr(root, path_item.strip(), None) + try: + root = getattr(root, path_item.strip(), None) + except Exception: + # Like annotationlib, ignore lazy imports that cannot be resolved. + return None if root is None: return None diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 17dee87599135b..2309ad296f86ac 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -4525,6 +4525,11 @@ def test_classvar_module_level_import(self): # won't exist on the instance. self.assertNotIn('not_iv4', c.__dict__) + def test_lazy_import_annotations(self): + from test.test_dataclasses import dataclass_lazy_str as m + self.assertEqual([f.name for f in fields(m.C)], ['x', 'y']) + self.assertNotIsInstance(vars(m)['t'], types.LazyImportType) + def test_text_annotations(self): from test.test_dataclasses import dataclass_textanno diff --git a/Lib/test/test_dataclasses/dataclass_lazy_broken.py b/Lib/test/test_dataclasses/dataclass_lazy_broken.py new file mode 100644 index 00000000000000..d9d29834fe1276 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_lazy_broken.py @@ -0,0 +1 @@ +raise RuntimeError("module fails to import") diff --git a/Lib/test/test_dataclasses/dataclass_lazy_str.py b/Lib/test/test_dataclasses/dataclass_lazy_str.py new file mode 100644 index 00000000000000..cd600fb6f38071 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_lazy_str.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +import dataclasses +lazy import typing as t +lazy from _dataclass_test_missing_module import Missing +lazy from test.test_dataclasses.dataclass_lazy_broken import Broken + +@dataclasses.dataclass +class C: + x: Missing | None = None + y: Broken | None = None + cv: t.ClassVar[int] = 0 diff --git a/Misc/NEWS.d/next/Library/2026-10-06-20-33-52.gh-issue-158936.flCm_m.rst b/Misc/NEWS.d/next/Library/2026-10-06-20-33-52.gh-issue-158936.flCm_m.rst new file mode 100644 index 00000000000000..5b023af8b1b91b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-06-20-33-52.gh-issue-158936.flCm_m.rst @@ -0,0 +1,3 @@ +:func:`~dataclasses.dataclass` no longer fails when a string annotation names a +:ref:`lazy import ` that cannot be resolved, matching the +behavior for non-string annotations.