From 89f35878e5f28d20e5cfd858d4293574df37c750 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Sun, 4 Oct 2026 20:56:02 -0700 Subject: [PATCH] gh-158936: Resolve lazy imports in dataclass string annotations like annotationlib 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. --- 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 bd317aa9b0f2f3..5ec91cddaa3e10 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 a89999bb97938c..2b4defe35f8a33 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.