deep-dive2024-12-05·8·244/348

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.3

Problem

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 defined

Error 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 defined

Error 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: A
from 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 localns parameter to provide forward references that are not yet available in the global namespace.
  • from __future__.annotations changes 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_CHECKING guards.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.