Python TypeHint runtime inspection 에러
How to handle Python type hint inspection errors at runtime — when get_type_hints() fails because of forward references or missing imports.
Python TypeHint runtime inspection 에러
Introduction
Python type hints were designed to be optional and non-intrusive. At runtime, they are simply annotations attached to function signatures and class attributes. However, when you try to inspect these hints programmatically using typing.get_type_hints(), you can run into errors that are difficult to understand.
This problem commonly appears when building frameworks, ORMs, or serialization libraries that need to read type information at runtime. I encountered it while building a custom data validation layer for a trading system in Lisbon, where I needed to inspect Pydantic-like models at runtime to generate database schemas.
Environment
Python 3.12.3Problem
Error 1: Forward reference not defined
from typing import get_type_hints
class Node:
value: int
children: list["Node"] # Forward reference
hints = get_type_hints(Node) # Error!NameError: name 'Node' is not definedError 2: Missing import in future.annotations
from __future__ import annotations # All annotations become strings
class Config:
database_url: str
timeout: float
# This fails because all annotations are now strings
# and get_type_hints tries to evaluate them
hints = get_type_hints(Config)NameError: name 'str' is not definedError 3: Circular import reference
# module_a.py
from module_b import B
class A:
partner: B
# module_b.py
from module_a import A
class B:
partner: Afrom typing import get_type_hints
hints = get_type_hints(A) # ImportError: cannot import name 'B'Error 4: Annotated type inspection failure
from typing import Annotated, get_type_hints
class User:
name: Annotated[str, "max_length=50"]
age: Annotated[int, "ge=0"]
# This raises TypeError in some Python versions
hints = get_type_hints(User, include_extras=True)Analysis
The core issue is that get_type_hints() evaluates string annotations back into actual types. This evaluation happens in the module's global namespace by default.
Why this happens:
When you write list["Node"], Python stores the string "Node" as the annotation, not the actual Node class. get_type_hints() tries to evaluate this string using eval() in the module's namespace. If the name is not yet defined in that namespace, the evaluation fails.
The from __future__.annotations problem:
This import makes ALL annotations lazy (stored as strings). While this solves forward reference issues for type checkers, it breaks get_type_hints() because the evaluation context may not have all necessary names.
The circular import problem:
If module A imports B and B imports A, you have a circular dependency. When get_type_hints() tries to evaluate B's type in A's namespace, it triggers the import, which may not be complete yet.
Solution
Strategy 1: Use typing.get_type_hints() with custom namespace
from typing import get_type_hints
import sys
class Node:
value: int
children: list["Node"]
# Pass the module's namespace explicitly
module_globals = sys.modules[Node.__module__].__dict__
hints = get_type_hints(Node, globalns=module_globals)Strategy 2: Use typing.get_type_hints() with localns for forward refs
from typing import get_type_hints
class Node:
value: int
children: list["Node"]
# Use localns to provide forward references
hints = get_type_hints(Node, localns={"Node": Node})Strategy 3: For __future__.annotations, use eval() manually
from __future__ import annotations
import typing
import sys
class Config:
database_url: str
timeout: float
# Get raw annotations (strings)
annotations = typing.get_type_hints(Config, include_extras=True)
# Or access __annotations__ directly
raw_annotations = Config.__annotations__
# {'database_url': 'str', 'timeout': 'float'}Strategy 4: Use typing.get_type_hints() with proper forward reference resolution
from typing import get_type_hints, ForwardRef
import sys
class Node:
value: int
children: list["Node"]
# Manually resolve forward references
def resolve_forward_refs(cls, localns=None):
hints = {}
for name, value in cls.__annotations__.items():
if isinstance(value, str):
value = ForwardRef(value)
if isinstance(value, ForwardRef):
try:
value = value._evaluate(
globalns=sys.modules[cls.__module__].__dict__,
localns=localns or {}
)
except NameError:
value = None
hints[name] = value
return hints
hints = resolve_forward_refs(Node, localns={"Node": Node})Strategy 5: Use inspect module for simpler cases
import inspect
import typing
class User:
name: str
age: int
# Get annotations directly without evaluation
annotations = typing.get_type_hints(User)
# Or use inspect for more control
sig = inspect.signature(User.__init__)
for param_name, param in sig.parameters.items():
if param.annotation != inspect.Parameter.empty:
print(f"{param_name}: {param.annotation}")Lessons Learned
get_type_hints()evaluates string annotations, so all referenced types must be available in the evaluation namespace.- Use
localnsparameter to provide forward references that are not yet available in the global namespace. from __future__.annotationschanges annotation behavior — annotations become strings and need explicit evaluation.- For framework development, prefer accessing
__annotations__directly and handling string evaluation yourself. - Circular imports can break type hint inspection — restructure imports to avoid cycles or use
TYPE_CHECKINGguards.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.