From cf85a28c933847f0636e745f95f3ddda43cc0977 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:18:02 -0700 Subject: [PATCH] Fix stringified bare ClassVar type hints Python 3.9 and 3.10 reject a bare ClassVar while evaluating a ForwardRef, so get_type_hints() raises for postponed annotations. Evaluate class annotations through the newer class-aware path and allow the bare ClassVar special form, with coverage for inherited hints. Fixes #643 --- CHANGELOG.md | 3 +++ src/test_typing_extensions.py | 12 ++++++++++++ src/typing_extensions.py | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2db6a6c..93ac32b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ No user-facing changes since 4.16.0rc2. +- Fix `get_type_hints()` raising `TypeError` for a stringified bare `ClassVar` + annotation on Python 3.9 and 3.10. + # Release 4.16.0rc2 (June 25, 2026) - Avoid a `DeprecationWarning` when `deprecated` is applied to a coroutine function on diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 996d466c..fb3f548e 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5648,6 +5648,18 @@ def test_compatibility(self): class GetTypeHintsTests(BaseTestCase): + def test_get_type_hints_lone_stringified_classvar(self): + class Parent: + parent: 'ClassVar' = 1 + + class Child(Parent): + child: 'typing.ClassVar' = 2 + + self.assertEqual( + get_type_hints(Child, globals()), + {'parent': ClassVar, 'child': typing.ClassVar}, + ) + def test_get_type_hints(self): def foobar(x: List['X']): ... X = Annotated[int, (1, 10)] diff --git a/src/typing_extensions.py b/src/typing_extensions.py index ced78373..48025961 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1602,9 +1602,37 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): - If two dict arguments are passed, they specify globals and locals, respectively. """ - hint = typing.get_type_hints( - obj, globalns=globalns, localns=localns, include_extras=True - ) + if sys.version_info < (3, 11) and isinstance(obj, type): + hint = {} + for base in reversed(obj.__mro__): + base_globals = ( + getattr(sys.modules.get(base.__module__), '__dict__', {}) + if globalns is None else globalns + ) + annotations = base.__dict__.get('__annotations__', {}) + if isinstance(annotations, _types.GetSetDescriptorType): + annotations = {} + base_locals = dict(vars(base)) if localns is None else localns + if localns is None and globalns is None: + base_globals, base_locals = base_locals, base_globals + for name, value in annotations.items(): + if value is None: + value = type(None) + elif isinstance(value, str): + evaluated = eval(value, base_globals, base_locals) + if evaluated is ClassVar: + value = evaluated + else: + value = ForwardRef( + value, is_argument=False, is_class=True + ) + hint[name] = typing._eval_type( + value, base_globals, base_locals + ) + else: + hint = typing.get_type_hints( + obj, globalns=globalns, localns=localns, include_extras=True + ) # Breakpoint: https://github.com/python/cpython/pull/30304 if sys.version_info < (3, 11): _clean_optional(obj, hint, globalns, localns)