Python get_type_hints: Resolve Annotations

Published on: August 29, 2026
Reading time: 5 minutes
High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.

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 wrapper

Classes 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: T

get_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 Customer

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

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python runtime_checkable: Protocol at Runtime

    Learn Python runtime_checkable to test Protocols with isinstance, understand its limits, and design safer structural contracts.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Unpack: Typed kwargs and Variadics

    Learn Python Unpack to type **kwargs with TypedDict, expand variadic tuples, and preserve precise callable signatures.

    Ler mais

    Tempo de leitura: 4 minutos
    29/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Required and NotRequired: Optional TypedDict Keys

    Learn Python Required and NotRequired to control mandatory and optional TypedDict keys without confusing absence with None.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ReadOnly: Protect TypedDict Fields

    Learn Python ReadOnly to protect TypedDict fields, model stable response contracts, and prevent accidental writes during static analysis.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python TypeAliasType: Runtime Type Aliases

    Learn Python TypeAliasType to create explicit aliases, inspect them at runtime, and model reusable generic APIs safely.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python overload: Precise Function Signatures

    Learn Python overload for precise signatures with Literal, None, generics, methods, and return types that depend on arguments.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026