typing.get_type_hints() retrieves annotations from functions, methods, classes, and modules in a form that is usually more useful than reading __annotations__ directly. It can resolve forward references, combine inherited class annotations, normalize special typing forms, and preserve Annotated metadata when requested.
That power requires care because resolving annotations may depend on namespaces and evaluate expressions. This guide covers decorators, validators, schemas, dependency injection, documentation, forward references, generics, errors, caching, circular imports, and security.
Basic __annotations__
def add(a: int, b: int) -> int:
return a + b
print(add.__annotations__)The dictionary contains declared annotations, but values may be strings or unresolved objects depending on Python version and module settings.
Using get_type_hints
from typing import get_type_hints
hints = get_type_hints(add)
print(hints)The result maps parameter names and the special return key to resolved type objects whenever possible.
Forward references
class User:
manager: "User | None"
print(User.__annotations__)
print(get_type_hints(User))Direct access may expose a string. get_type_hints attempts to resolve User and construct the actual union.
Deferred annotations
When a module uses deferred annotation behavior, expressions may remain unevaluated. get_type_hints is a centralized way to resolve them, provided referenced names are available.
Global and local namespaces
get_type_hints(obj, globalns=globals_map, localns=locals_map)Explicit namespaces help with dynamically created classes, nested functions, and tooling that inspects objects outside their original module. Supply accurate and minimal mappings.
Resolution failures
class Order:
customer: "Customer"If Customer is unavailable, get_type_hints may raise NameError. Tooling should catch failures, identify the problematic annotation, and decide whether unresolved values are acceptable.
Annotated metadata is stripped by default
from typing import Annotated
def age(value: Annotated[int, "0 through 130"]) -> None:
...Without extra options, the result may contain only int.
Preserving extras
hints = get_type_hints(age, include_extras=True)include_extras=True preserves Annotated, Required, NotRequired, and related qualifiers for schema and validation tools. See the Python Annotated guide.
Inspecting Annotated
from typing import get_args, get_origin
annotation = hints["value"]
print(get_origin(annotation))
print(get_args(annotation))Use typing APIs instead of parsing string representations. get_args() returns the base type and metadata.
Decorated functions
Decorators should use functools.wraps to preserve metadata and __wrapped__. Poorly implemented wrappers can hide the original annotations and signature.
from functools import wraps
def log(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapperClasses and methods
class Configuration:
host: str
port: int
print(get_type_hints(Configuration))For classes, the function combines annotations along the method resolution order. Derived definitions take precedence.
Inheritance
class Base:
id: int
class Derived(Base):
name: str
get_type_hints(Derived)The result can include both id and name. A derived annotation replaces a base annotation with the same name.
ClassVar and Final
With extras preserved, frameworks can distinguish instance fields from class variables or final declarations. Each framework must decide whether to ignore, document, or handle these qualifiers specially.
TypedDict
get_type_hints retrieves value types, while required and optional key presence also depends on __required_keys__ and __optional_keys__. Schema generators should combine both sources.
Dataclasses
For dataclasses, get_type_hints resolves field types and dataclasses.fields() supplies defaults, factories, flags, and metadata. Serializers usually need both.
Type aliases
Modern aliases may remain named entities or be evaluated depending on the inspection path. Tools should decide when to expand TypeAliasType and when to preserve its public name. See Python TypeAliasType.
Generics
from typing import Generic, TypeVar
T = TypeVar("T")
class Box(Generic[T]):
value: Tget_type_hints on the class retrieves T, but it does not automatically substitute int for every specialized Box[int] context. Concrete substitution requires tracking origins, arguments, and generic bases.
Unsupported objects
Not every runtime object exposes meaningful annotations. Built-ins and C extension functions may lack complete metadata. Validate inputs and produce clear diagnostic messages.
Evaluation and security
Annotations may contain expressions. Resolving them can execute code in some situations. Do not treat annotations from untrusted sources as inert data. Inspect trusted code, restrict namespaces, and avoid importing unknown modules solely to resolve hints.
Not a direct validator
get_type_hints describes the declared contract but does not test values. A validator must interpret unions, collections, TypedDict, Protocol, Annotated, recursion, and custom metadata.
Caching
Repeated resolution can be expensive. Frameworks often cache results per function or class. Dynamic mutation of annotations complicates invalidation and is best avoided.
Circular imports
Forward references reduce top-level imports, but get_type_hints still needs names during runtime resolution. Organize model modules carefully, use explicit namespaces, and delay resolution until application initialization when necessary.
TYPE_CHECKING imports
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from package import CustomerThe import exists only for static analysis. At runtime, get_type_hints may not find Customer. Provide the name through an actual runtime import or globalns if resolution is required.
A simple call validator
from inspect import signature
from typing import get_type_hints
def validate_call(func, *args, **kwargs):
sig = signature(func)
bound = sig.bind(*args, **kwargs)
hints = get_type_hints(func)
for name, value in bound.arguments.items():
expected = hints.get(name)
if isinstance(expected, type) and not isinstance(value, expected):
raise TypeError(f"{name} must be {expected.__name__}")This demonstration supports only simple classes. Unions, generics, Protocols, and nested structures require a full validation engine.
Documentation generation
A documentation tool can combine inspect.signature(), docstrings, defaults, and get_type_hints. Preserve public aliases and Annotated metadata when they improve the generated contract.
Common mistakes
- Reading only __annotations__: forward references may remain strings.
- Forgetting include_extras: metadata and qualifiers may disappear.
- Ignoring NameError: names may not be resolvable.
- Evaluating untrusted code: annotations are not necessarily safe data.
- Treating hints as validation: values still need checking.
- Resolving on every call: caching may be necessary.
Complete example: handler registry
from typing import get_type_hints
class Event: ...
class Context: ...
handlers: dict[type[Event], object] = {}
def handler(func):
hints = get_type_hints(func, include_extras=True)
event = hints.get("event")
result = hints.get("return")
if not isinstance(event, type) or not issubclass(event, Event):
raise TypeError("invalid event parameter")
if result is not None and result is not type(None):
raise TypeError("handler must return None")
handlers[event] = func
return func
@handler
def process(event: Event, context: Context) -> None:
...The decorator uses annotations as an intentional registry API. A production implementation would also check parameter names, count, subclasses, and error messages.
When to avoid introspection
If an interface can be registered explicitly, passing arguments to a registry is often simpler and more predictable. Use get_type_hints when annotations are intentionally part of the framework API, such as serialization, dependency injection, validation, and documentation.
Conclusion
get_type_hints() is the primary runtime tool for retrieving resolved annotations. It handles forward references, class inheritance, and typing extras more reliably than raw __annotations__.
The official Python get_type_hints documentation describes its parameters. Use controlled namespaces, preserve extras when needed, handle resolution failures, cache responsibly, and never evaluate untrusted annotations without considering code execution risks.







