Skip to content

Commit cf85a28

Browse files
committed
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
1 parent 83400e9 commit cf85a28

3 files changed

Lines changed: 46 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
No user-facing changes since 4.16.0rc2.
44

5+
- Fix `get_type_hints()` raising `TypeError` for a stringified bare `ClassVar`
6+
annotation on Python 3.9 and 3.10.
7+
58
# Release 4.16.0rc2 (June 25, 2026)
69

710
- Avoid a `DeprecationWarning` when `deprecated` is applied to a coroutine function on

src/test_typing_extensions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5648,6 +5648,18 @@ def test_compatibility(self):
56485648

56495649

56505650
class GetTypeHintsTests(BaseTestCase):
5651+
def test_get_type_hints_lone_stringified_classvar(self):
5652+
class Parent:
5653+
parent: 'ClassVar' = 1
5654+
5655+
class Child(Parent):
5656+
child: 'typing.ClassVar' = 2
5657+
5658+
self.assertEqual(
5659+
get_type_hints(Child, globals()),
5660+
{'parent': ClassVar, 'child': typing.ClassVar},
5661+
)
5662+
56515663
def test_get_type_hints(self):
56525664
def foobar(x: List['X']): ...
56535665
X = Annotated[int, (1, 10)]

src/typing_extensions.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,9 +1602,37 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False):
16021602
- If two dict arguments are passed, they specify globals and
16031603
locals, respectively.
16041604
"""
1605-
hint = typing.get_type_hints(
1606-
obj, globalns=globalns, localns=localns, include_extras=True
1607-
)
1605+
if sys.version_info < (3, 11) and isinstance(obj, type):
1606+
hint = {}
1607+
for base in reversed(obj.__mro__):
1608+
base_globals = (
1609+
getattr(sys.modules.get(base.__module__), '__dict__', {})
1610+
if globalns is None else globalns
1611+
)
1612+
annotations = base.__dict__.get('__annotations__', {})
1613+
if isinstance(annotations, _types.GetSetDescriptorType):
1614+
annotations = {}
1615+
base_locals = dict(vars(base)) if localns is None else localns
1616+
if localns is None and globalns is None:
1617+
base_globals, base_locals = base_locals, base_globals
1618+
for name, value in annotations.items():
1619+
if value is None:
1620+
value = type(None)
1621+
elif isinstance(value, str):
1622+
evaluated = eval(value, base_globals, base_locals)
1623+
if evaluated is ClassVar:
1624+
value = evaluated
1625+
else:
1626+
value = ForwardRef(
1627+
value, is_argument=False, is_class=True
1628+
)
1629+
hint[name] = typing._eval_type(
1630+
value, base_globals, base_locals
1631+
)
1632+
else:
1633+
hint = typing.get_type_hints(
1634+
obj, globalns=globalns, localns=localns, include_extras=True
1635+
)
16081636
# Breakpoint: https://github.com/python/cpython/pull/30304
16091637
if sys.version_info < (3, 11):
16101638
_clean_optional(obj, hint, globalns, localns)

0 commit comments

Comments
 (0)