Skip to content

API reference

Complete reference for all public classes and functions in the typing-graph library. This reference is auto-generated from source docstrings using mkdocstrings.

Contents


Core inspection functions

Entry points for inspecting type annotations, classes, functions, modules, and type parameters.

Functions:

Name Description
inspect_type

Inspect any type annotation and return the corresponding TypeNode.

inspect_class

Inspect a class and return the appropriate TypeNode.

inspect_dataclass

Inspect a dataclass specifically.

inspect_enum

Inspect an Enum specifically.

inspect_named_tuple

Inspect a NamedTuple specifically.

inspect_protocol

Inspect a Protocol specifically.

inspect_typed_dict

Inspect a TypedDict specifically.

inspect_function

Inspect a function and return a FunctionNode.

inspect_signature

Inspect a callable's signature.

inspect_module

Inspect all public types in a module.

inspect_type_alias

Inspect a type alias.

inspect_type_param

Inspect a type parameter.

typing_graph.inspect_type

inspect_type(
    annotation: Any,
    *,
    config: InspectConfig | None = None,
    use_cache: bool = True,
    source: type | Callable[..., Any] | ModuleType | None = None
) -> TypeNode

Parameters:

Name Type Description Default
annotation Any

Any valid type annotation.

required
config InspectConfig | None

Introspection configuration. Uses defaults if None.

None
use_cache bool

Whether to use the global cache (default True). Note: Cache is only used when config is None or DEFAULT_CONFIG.

True
source type | Callable[..., Any] | ModuleType | None

Optional context object for namespace auto-detection. Can be a class, function, or module. When provided and config.auto_namespace is True, namespaces will be extracted from this object for forward reference resolution. When source is provided, the global cache is bypassed regardless of use_cache setting.

None

Returns:

Type Description
TypeNode

A TypeNode representing the annotation's structure.

Raises:

Type Description
TypeError

If source is not None and is not a class, callable, or module.

Source code in src/typing_graph/_inspect_type.py
def inspect_type(
    annotation: Any,
    *,
    config: InspectConfig | None = None,
    use_cache: bool = True,
    source: type | Callable[..., Any] | ModuleType | None = None,
) -> TypeNode:
    """Inspect any type annotation and return the corresponding TypeNode.

    This is the primary workhorse function. It handles:
    - Concrete types (int, str, MyClass)
    - Generic types (list, Dict, List[int], Dict[str, T])
    - Special forms (Any, Union, Optional, Literal, etc.)
    - Type variables (TypeVar, ParamSpec, TypeVarTuple)
    - Callable types
    - Annotated types
    - Forward references (strings or ForwardRefNode objects)

    Args:
        annotation: Any valid type annotation.
        config: Introspection configuration. Uses defaults if None.
        use_cache: Whether to use the global cache (default True).
            Note: Cache is only used when config is None or DEFAULT_CONFIG.
        source: Optional context object for namespace auto-detection.
            Can be a class, function, or module. When provided and
            config.auto_namespace is True, namespaces will be extracted
            from this object for forward reference resolution. When source
            is provided, the global cache is bypassed regardless of
            use_cache setting.

    Returns:
        A TypeNode representing the annotation's structure.

    Raises:
        TypeError: If source is not None and is not a class, callable,
            or module.
    """
    _register_type_inspectors()

    config = config if config is not None else DEFAULT_CONFIG

    # Handle source parameter for namespace extraction
    if source is not None:
        # Validate source type - pyright correctly identifies this as unreachable
        # based on the type signature, but we keep this guard for runtime safety
        # when called from untyped code
        if not (isinstance(source, type | ModuleType) or callable(source)):
            source_type = type(source).__name__  # pyright: ignore[reportUnreachable]
            msg = f"source must be a class, callable, or module, got {source_type!r}"
            raise TypeError(msg)

        # Extract and merge namespaces if auto_namespace is enabled
        if config.auto_namespace:
            auto_globalns, auto_localns = extract_namespace(source)
            merged_globalns, merged_localns = merge_namespaces(
                auto_globalns,
                auto_localns,
                config.globalns,
                config.localns,
            )
            # Create modified config with merged namespaces
            config = dataclasses.replace(
                config,
                globalns=merged_globalns,
                localns=merged_localns,
            )

        # Always bypass cache when source is provided
        ctx = InspectContext(config=config)
        return _inspect_type(annotation, ctx)

    # Use lru_cache only with default config (custom configs may need
    # different forward ref resolution via globalns/localns)
    if use_cache and config is DEFAULT_CONFIG:
        return _inspect_type_cached(_TypeKey(annotation))

    ctx = InspectContext(config=config)
    return _inspect_type(annotation, ctx)

typing_graph.inspect_class

inspect_class(cls: type, *, config: InspectConfig | None = None) -> ClassInspectResult

Parameters:

Name Type Description Default
cls type

The class to inspect.

required
config InspectConfig | None

Introspection configuration. Uses defaults if None. When config.auto_namespace is True (default), namespaces are automatically extracted from the class for forward reference resolution. User-provided globalns/localns take precedence.

None

Returns:

Type Description
ClassInspectResult

A specialized TypeNode based on the class type.

Source code in src/typing_graph/_inspect_class.py
def inspect_class(
    cls: type,
    *,
    config: InspectConfig | None = None,
) -> ClassInspectResult:
    """Inspect a class and return the appropriate TypeNode.

    Automatically detects and returns specialized nodes for:
    - dataclasses -> DataclassNode
    - TypedDict -> TypedDictNode
    - NamedTuple -> NamedTupleNode
    - Protocol -> ProtocolNode
    - Enum -> EnumNode
    - Regular classes -> ClassNode

    Args:
        cls: The class to inspect.
        config: Introspection configuration. Uses defaults if None.
            When config.auto_namespace is True (default), namespaces are
            automatically extracted from the class for forward reference
            resolution. User-provided globalns/localns take precedence.

    Returns:
        A specialized TypeNode based on the class type.
    """
    config = config if config is not None else DEFAULT_CONFIG
    config = apply_class_namespace(cls, config)
    ctx = InspectContext(config=config)

    # Detect class type and dispatch using TypeIs wrappers for type narrowing
    if is_dataclass_class(cls):
        return _inspect_dataclass(cls, ctx)
    if is_typeddict_class(cls):
        return _inspect_typed_dict(cls, ctx)
    if is_namedtuple_class(cls):
        return _inspect_named_tuple(cls, ctx)
    if is_protocol_class(cls):
        return _inspect_protocol(cls, ctx)
    if is_enum_class(cls):
        return _inspect_enum(cls, ctx)

    # cls type is narrowed to Unknown after exhaustive TypeIs checks above;
    # it's still a valid type but pyright loses track of the original type
    return _inspect_class(cls, ctx)  # pyright: ignore[reportUnknownArgumentType]

typing_graph.inspect_dataclass

inspect_dataclass(cls: type, *, config: InspectConfig | None = None) -> DataclassNode

Parameters:

Name Type Description Default
cls type

The dataclass to inspect.

required
config InspectConfig | None

Introspection configuration. Uses defaults if None. When config.auto_namespace is True (default), namespaces are automatically extracted from the dataclass for forward reference resolution. User-provided globalns/localns take precedence.

None

Returns:

Type Description
DataclassNode

A DataclassNode node representing the dataclass.

Raises:

Type Description
TypeError

If cls is not a dataclass.

Source code in src/typing_graph/_inspect_class.py
def inspect_dataclass(
    cls: type,
    *,
    config: InspectConfig | None = None,
) -> DataclassNode:
    """Inspect a dataclass specifically.

    Args:
        cls: The dataclass to inspect.
        config: Introspection configuration. Uses defaults if None.
            When config.auto_namespace is True (default), namespaces are
            automatically extracted from the dataclass for forward reference
            resolution. User-provided globalns/localns take precedence.

    Returns:
        A DataclassNode node representing the dataclass.

    Raises:
        TypeError: If cls is not a dataclass.
    """
    if not is_dataclass_class(cls):
        msg = f"{cls} is not a dataclass"
        raise TypeError(msg)

    config = config if config is not None else DEFAULT_CONFIG
    config = apply_class_namespace(cls, config)

    ctx = InspectContext(config=config)
    return _inspect_dataclass(cls, ctx)

typing_graph.inspect_enum

inspect_enum(cls: type, *, config: InspectConfig | None = None) -> EnumNode

Parameters:

Name Type Description Default
cls type

The Enum to inspect.

required
config InspectConfig | None

Introspection configuration. Uses defaults if None. When config.auto_namespace is True (default), namespaces are automatically extracted from the Enum for forward reference resolution. User-provided globalns/localns take precedence.

None

Returns:

Type Description
EnumNode

An EnumNode node representing the Enum.

Raises:

Type Description
TypeError

If cls is not an Enum.

Source code in src/typing_graph/_inspect_class.py
def inspect_enum(
    cls: type,
    *,
    config: InspectConfig | None = None,
) -> EnumNode:
    """Inspect an Enum specifically.

    Args:
        cls: The Enum to inspect.
        config: Introspection configuration. Uses defaults if None.
            When config.auto_namespace is True (default), namespaces are
            automatically extracted from the Enum for forward reference
            resolution. User-provided globalns/localns take precedence.

    Returns:
        An EnumNode node representing the Enum.

    Raises:
        TypeError: If cls is not an Enum.
    """
    if not is_enum_class(cls):
        msg = f"{cls} is not an Enum"
        raise TypeError(msg)

    config = config if config is not None else DEFAULT_CONFIG
    config = apply_class_namespace(cls, config)

    ctx = InspectContext(config=config)
    return _inspect_enum(cls, ctx)

typing_graph.inspect_named_tuple

inspect_named_tuple(
    cls: type, *, config: InspectConfig | None = None
) -> NamedTupleNode

Parameters:

Name Type Description Default
cls type

The NamedTuple to inspect.

required
config InspectConfig | None

Introspection configuration. Uses defaults if None. When config.auto_namespace is True (default), namespaces are automatically extracted from the NamedTuple for forward reference resolution. User-provided globalns/localns take precedence.

None

Returns:

Type Description
NamedTupleNode

A NamedTupleNode node representing the NamedTuple.

Raises:

Type Description
TypeError

If cls is not a NamedTuple.

Source code in src/typing_graph/_inspect_class.py
def inspect_named_tuple(
    cls: type,
    *,
    config: InspectConfig | None = None,
) -> NamedTupleNode:
    """Inspect a NamedTuple specifically.

    Args:
        cls: The NamedTuple to inspect.
        config: Introspection configuration. Uses defaults if None.
            When config.auto_namespace is True (default), namespaces are
            automatically extracted from the NamedTuple for forward reference
            resolution. User-provided globalns/localns take precedence.

    Returns:
        A NamedTupleNode node representing the NamedTuple.

    Raises:
        TypeError: If cls is not a NamedTuple.
    """
    if not is_namedtuple_class(cls):
        msg = f"{cls} is not a NamedTuple"
        raise TypeError(msg)

    config = config if config is not None else DEFAULT_CONFIG
    config = apply_class_namespace(cls, config)

    ctx = InspectContext(config=config)
    return _inspect_named_tuple(cls, ctx)

typing_graph.inspect_protocol

inspect_protocol(cls: type, *, config: InspectConfig | None = None) -> ProtocolNode

Parameters:

Name Type Description Default
cls type

The Protocol to inspect.

required
config InspectConfig | None

Introspection configuration. Uses defaults if None. When config.auto_namespace is True (default), namespaces are automatically extracted from the Protocol for forward reference resolution. User-provided globalns/localns take precedence.

None

Returns:

Type Description
ProtocolNode

A ProtocolNode node representing the Protocol.

Raises:

Type Description
TypeError

If cls is not a Protocol.

Source code in src/typing_graph/_inspect_class.py
def inspect_protocol(
    cls: type,
    *,
    config: InspectConfig | None = None,
) -> ProtocolNode:
    """Inspect a Protocol specifically.

    Args:
        cls: The Protocol to inspect.
        config: Introspection configuration. Uses defaults if None.
            When config.auto_namespace is True (default), namespaces are
            automatically extracted from the Protocol for forward reference
            resolution. User-provided globalns/localns take precedence.

    Returns:
        A ProtocolNode node representing the Protocol.

    Raises:
        TypeError: If cls is not a Protocol.
    """
    if not is_protocol_class(cls):
        msg = f"{cls} is not a Protocol"
        raise TypeError(msg)

    config = config if config is not None else DEFAULT_CONFIG
    config = apply_class_namespace(cls, config)

    ctx = InspectContext(config=config)
    return _inspect_protocol(cls, ctx)

typing_graph.inspect_typed_dict

inspect_typed_dict(cls: type, *, config: InspectConfig | None = None) -> TypedDictNode

Parameters:

Name Type Description Default
cls type

The TypedDict to inspect.

required
config InspectConfig | None

Introspection configuration. Uses defaults if None. When config.auto_namespace is True (default), namespaces are automatically extracted from the TypedDict for forward reference resolution. User-provided globalns/localns take precedence.

None

Returns:

Type Description
TypedDictNode

A TypedDictNode node representing the TypedDict.

Raises:

Type Description
TypeError

If cls is not a TypedDict.

Source code in src/typing_graph/_inspect_class.py
def inspect_typed_dict(
    cls: type,
    *,
    config: InspectConfig | None = None,
) -> TypedDictNode:
    """Inspect a TypedDict specifically.

    Args:
        cls: The TypedDict to inspect.
        config: Introspection configuration. Uses defaults if None.
            When config.auto_namespace is True (default), namespaces are
            automatically extracted from the TypedDict for forward reference
            resolution. User-provided globalns/localns take precedence.

    Returns:
        A TypedDictNode node representing the TypedDict.

    Raises:
        TypeError: If cls is not a TypedDict.
    """
    if not is_typeddict_class(cls):
        msg = f"{cls} is not a TypedDict"
        raise TypeError(msg)

    config = config if config is not None else DEFAULT_CONFIG
    config = apply_class_namespace(cls, config)

    ctx = InspectContext(config=config)
    return _inspect_typed_dict(cls, ctx)

typing_graph.inspect_function

inspect_function(
    func: Callable[..., Any], *, config: InspectConfig | None = None
) -> FunctionNode

Parameters:

Name Type Description Default
func Callable[..., Any]

The function to inspect.

required
config InspectConfig | None

Introspection configuration. Uses defaults if None.

None

Returns:

Type Description
FunctionNode

A FunctionNode representing the function's structure.

Source code in src/typing_graph/_inspect_function.py
def inspect_function(
    func: "Callable[..., Any]",
    *,
    config: InspectConfig | None = None,
) -> FunctionNode:
    """Inspect a function and return a FunctionNode.

    When ``config.auto_namespace`` is True (the default), namespaces are
    automatically extracted from the function for forward reference resolution.
    User-provided namespaces in the config take precedence over auto-detected
    values.

    Args:
        func: The function to inspect.
        config: Introspection configuration. Uses defaults if None.

    Returns:
        A FunctionNode representing the function's structure.
    """
    config = config if config is not None else DEFAULT_CONFIG
    config = apply_function_namespace(func, config)
    ctx = InspectContext(config=config)

    sig = _inspect_signature(func, ctx)

    # Determine function properties
    is_async = inspect.iscoroutinefunction(func)
    is_generator = inspect.isgeneratorfunction(func)

    # Get decorators (best effort)
    decorators: list[str] = []
    if is_async:
        decorators.append("async")
    if isinstance(func, staticmethod):
        decorators.append("staticmethod")
    if isinstance(func, classmethod):
        decorators.append("classmethod")

    return FunctionNode(
        name=getattr(func, "__name__", "<anonymous>"),
        signature=sig,
        is_async=is_async,
        is_generator=is_generator,
        decorators=tuple(decorators),
        source=get_source_location(func, ctx.config),
    )

typing_graph.inspect_signature

inspect_signature(
    callable_obj: Callable[..., Any],
    *,
    config: InspectConfig | None = None,
    follow_wrapped: bool = True
) -> SignatureNode

Parameters:

Name Type Description Default
callable_obj Callable[..., Any]

The callable to inspect.

required
config InspectConfig | None

Introspection configuration. Uses defaults if None.

None
follow_wrapped bool

Whether to unwrap decorated functions (default True).

True

Returns:

Type Description
SignatureNode

A SignatureNode representing the callable's signature.

Source code in src/typing_graph/_inspect_function.py
def inspect_signature(
    callable_obj: "Callable[..., Any]",
    *,
    config: InspectConfig | None = None,
    follow_wrapped: bool = True,
) -> SignatureNode:
    """Inspect a callable's signature.

    When ``config.auto_namespace`` is True (the default), namespaces are
    automatically extracted from the callable for forward reference resolution.
    User-provided namespaces in the config take precedence over auto-detected
    values.

    Args:
        callable_obj: The callable to inspect.
        config: Introspection configuration. Uses defaults if None.
        follow_wrapped: Whether to unwrap decorated functions (default True).

    Returns:
        A SignatureNode representing the callable's signature.
    """
    config = config if config is not None else DEFAULT_CONFIG
    config = apply_function_namespace(callable_obj, config)
    ctx = InspectContext(config=config)
    return _inspect_signature(callable_obj, ctx, follow_wrapped=follow_wrapped)

typing_graph.inspect_module

inspect_module(
    module: ModuleType,
    *,
    config: InspectConfig | None = None,
    include_imported: bool = False
) -> ModuleTypes

Parameters:

Name Type Description Default
module ModuleType

The module to inspect.

required
config InspectConfig | None

Introspection configuration. Uses defaults if None.

None
include_imported bool

Whether to include items imported from other modules (default False).

False

Returns:

Type Description
ModuleTypes

A ModuleTypes containing all discovered types organized by category.

Source code in src/typing_graph/_inspect_module.py
def inspect_module(
    module: "types.ModuleType",
    *,
    config: InspectConfig | None = None,
    include_imported: bool = False,
) -> ModuleTypes:
    """Inspect all public types in a module.

    Discovers and inspects all classes, functions, type aliases, type variables,
    and annotated constants in a module.

    Args:
        module: The module to inspect.
        config: Introspection configuration. Uses defaults if None.
        include_imported: Whether to include items imported from other modules
            (default False).

    Returns:
        A ModuleTypes containing all discovered types organized by category.
    """
    config = config if config is not None else DEFAULT_CONFIG
    ctx = InspectContext(config=config)
    format_val = ctx.config.get_format()

    result = ModuleTypes()
    module_name = module.__name__

    # Get module-level annotations
    try:
        module_annotations = get_annotations(
            module,
            format=format_val,
            globals=vars(module),
            locals=ctx.config.localns,
        )
    except Exception:  # noqa: BLE001 - Intentionally broad for robust module handling
        _logger.debug(
            "Failed to get annotations for module %s", module_name, exc_info=True
        )
        module_annotations = {}

    # Process module contents
    for name in dir(module):
        if not config.include_private and name.startswith("_"):
            continue

        try:
            obj = getattr(module, name)
        except AttributeError:  # pragma: no cover
            continue

        # Skip imported items unless requested
        if not include_imported:
            obj_module = getattr(obj, "__module__", None)
            if obj_module is not None and obj_module != module_name:
                continue

        # Classify and inspect
        _inspect_module_item(name, obj, config, ctx, result)

    # Process module annotations as constants
    for name, ann in module_annotations.items():
        if not config.include_private and name.startswith("_"):
            continue

        if name not in result.classes and name not in result.functions:
            result.constants[name] = _inspect_type(ann, ctx.child())

    return result

typing_graph.inspect_type_alias

inspect_type_alias(
    alias: Any, *, name: str | None = None, config: InspectConfig | None = None
) -> GenericAliasNode | TypeAliasNode

Parameters:

Name Type Description Default
alias Any

The type alias to inspect.

required
name str | None

Optional name for the alias (used for simple aliases).

None
config InspectConfig | None

Introspection configuration. Uses defaults if None.

None

Returns:

Type Description
GenericAliasNode | TypeAliasNode

A GenericAliasNode for PEP 695 TypeAliasType, or TypeAliasNode for

GenericAliasNode | TypeAliasNode

simple type aliases.

Source code in src/typing_graph/_inspect_type.py
def inspect_type_alias(
    alias: Any,
    *,
    name: str | None = None,
    config: InspectConfig | None = None,
) -> GenericAliasNode | TypeAliasNode:
    """Inspect a type alias.

    Args:
        alias: The type alias to inspect.
        name: Optional name for the alias (used for simple aliases).
        config: Introspection configuration. Uses defaults if None.

    Returns:
        A GenericAliasNode for PEP 695 TypeAliasType, or TypeAliasNode for
        simple type aliases.
    """
    config = config if config is not None else DEFAULT_CONFIG
    ctx = InspectContext(config=config)

    # PEP 695 TypeAliasType (may be None in Python 3.10)
    if TypeAliasType is not None and isinstance(alias, TypeAliasType):  # pyright: ignore[reportUnnecessaryComparison]
        return _inspect_type_alias_type(alias, ctx)

    # Simple type alias
    value = _inspect_type(alias, ctx.child())
    alias_name = name or getattr(alias, "__name__", "TypeAlias")

    return TypeAliasNode(
        name=alias_name,
        value=value,
    )

typing_graph.inspect_type_param

inspect_type_param(
    param: TypeVar, *, config: InspectConfig | None = None
) -> TypeVarNode
inspect_type_param(
    param: ParamSpec, *, config: InspectConfig | None = None
) -> ParamSpecNode
inspect_type_param(
    param: TypeVarTuple, *, config: InspectConfig | None = None
) -> TypeVarTupleNode
inspect_type_param(
    param: TypeVar | ParamSpec | TypeVarTuple, *, config: InspectConfig | None = None
) -> TypeVarNode | ParamSpecNode | TypeVarTupleNode

Parameters:

Name Type Description Default
param TypeVar | ParamSpec | TypeVarTuple

A TypeVar, ParamSpec, or TypeVarTuple to inspect.

required
config InspectConfig | None

Introspection configuration. Uses defaults if None.

None

Returns:

Type Description
TypeVarNode | ParamSpecNode | TypeVarTupleNode

The corresponding TypeParamNode (TypeVarNode, ParamSpecNode,

TypeVarNode | ParamSpecNode | TypeVarTupleNode

or TypeVarTupleNode).

Raises:

Type Description
TypeError

If param is not a known type parameter type.

Source code in src/typing_graph/_inspect_type.py
def inspect_type_param(
    param: TypeVar | ParamSpec | TypeVarTuple,
    *,
    config: InspectConfig | None = None,
) -> TypeVarNode | ParamSpecNode | TypeVarTupleNode:
    """Inspect a type parameter.

    Args:
        param: A TypeVar, ParamSpec, or TypeVarTuple to inspect.
        config: Introspection configuration. Uses defaults if None.

    Returns:
        The corresponding TypeParamNode (TypeVarNode, ParamSpecNode,
        or TypeVarTupleNode).

    Raises:
        TypeError: If param is not a known type parameter type.
    """
    config = config if config is not None else DEFAULT_CONFIG
    ctx = InspectContext(config=config)

    # Check TypeVarTuple BEFORE TypeVar because TypeVarTuple is a TypeVar subclass
    # on some Python versions
    if isinstance(param, TypeVarTuple):
        return _inspect_typevartuple(param, ctx)
    if isinstance(param, ParamSpec):
        return _inspect_paramspec(param, ctx)
    # After TypeVarTuple and ParamSpec, TypeVar remains
    return _inspect_typevar(param, ctx)

Traversal

The primary traversal function for depth-first iteration over type graphs.

Functions:

Name Description
walk

Traverse the type graph depth-first, yielding unique nodes.

typing_graph.walk

walk(
    node: TypeNode,
    *,
    predicate: Callable[[TypeNode], TypeIs[TypeNodeT]],
    max_depth: int | None = None
) -> Iterator[TypeNodeT]
walk(
    node: TypeNode,
    *,
    predicate: Callable[[TypeNode], bool] | None = None,
    max_depth: int | None = None
) -> Iterator[TypeNode]
walk(
    node: TypeNode,
    *,
    predicate: Callable[[TypeNode], bool] | None = None,
    max_depth: int | None = None
) -> Iterator[TypeNode]

Parameters:

Name Type Description Default
node TypeNode

The root node to start traversal from.

required
predicate Callable[[TypeNode], bool] | None

Optional filter function. If provided, only nodes for which predicate(node) returns True are yielded. When using a TypeIs predicate, the return type is narrowed accordingly.

None
max_depth int | None

Maximum traversal depth. If None, no limit is imposed. When depth exceeds this limit, traversal stops descending into that branch but continues with other branches. Depth 0 yields only the root node.

None

Yields:

Type Description
TypeNode

TypeNode instances matching the predicate (or all nodes if no predicate).

Raises:

Type Description
TraversalError

If max_depth is negative.

Examples:

Basic traversal of all nodes:

>>> from typing_graph import inspect_type
>>> from typing_graph import walk
>>> node = inspect_type(list[int])
>>> len(list(walk(node)))
3

Filtered traversal with type narrowing:

>>> from typing_graph import inspect_type, walk, ConcreteNode, TypeNode
>>> from typing_extensions import TypeIs
>>> def is_concrete(n: TypeNode) -> TypeIs[ConcreteNode]:
...     return isinstance(n, ConcreteNode)
>>> node = inspect_type(dict[str, int])
>>> concrete_nodes = list(walk(node, predicate=is_concrete))
>>> all(isinstance(n, ConcreteNode) for n in concrete_nodes)
True

Depth-limited traversal:

>>> from typing_graph import inspect_type, walk
>>> node = inspect_type(list[dict[str, int]])
>>> # Depth 0 yields only the root
>>> len(list(walk(node, max_depth=0)))
1
Source code in src/typing_graph/_walk.py
def walk(
    node: "TypeNode",
    *,
    predicate: "Callable[[TypeNode], bool] | None" = None,
    max_depth: int | None = None,
) -> "Iterator[TypeNode]":
    """Traverse the type graph depth-first, yielding unique nodes.

    This function uses an iterative stack to prevent recursion depth errors.
    Each node is visited exactly once (by object identity).

    Args:
        node: The root node to start traversal from.
        predicate: Optional filter function. If provided, only nodes for which
            predicate(node) returns True are yielded. When using a TypeIs
            predicate, the return type is narrowed accordingly.
        max_depth: Maximum traversal depth. If None, no limit is imposed.
            When depth exceeds this limit, traversal stops descending
            into that branch but continues with other branches.
            Depth 0 yields only the root node.

    Yields:
        TypeNode instances matching the predicate (or all nodes if no predicate).

    Raises:
        TraversalError: If max_depth is negative.

    Note:
        The predicate and max_depth parameters are keyword-only to prevent
        accidental positional usage and to allow future parameters to be
        added without breaking changes.

    Examples:
        Basic traversal of all nodes:

        >>> from typing_graph import inspect_type
        >>> from typing_graph import walk
        >>> node = inspect_type(list[int])
        >>> len(list(walk(node)))
        3

        Filtered traversal with type narrowing:

        >>> from typing_graph import inspect_type, walk, ConcreteNode, TypeNode
        >>> from typing_extensions import TypeIs
        >>> def is_concrete(n: TypeNode) -> TypeIs[ConcreteNode]:
        ...     return isinstance(n, ConcreteNode)
        >>> node = inspect_type(dict[str, int])
        >>> concrete_nodes = list(walk(node, predicate=is_concrete))
        >>> all(isinstance(n, ConcreteNode) for n in concrete_nodes)
        True

        Depth-limited traversal:

        >>> from typing_graph import inspect_type, walk
        >>> node = inspect_type(list[dict[str, int]])
        >>> # Depth 0 yields only the root
        >>> len(list(walk(node, max_depth=0)))
        1
    """
    # Validate max_depth
    if max_depth is not None and max_depth < 0:
        msg = f"max_depth must be non-negative, got {max_depth}"
        raise TraversalError(msg)

    visited: set[int] = set()

    # Initialize stack with (node, depth) tuples
    stack: deque[tuple[TypeNode, int]] = deque([(node, 0)])

    while stack:
        current, depth = stack.pop()
        node_id = id(current)

        # Skip already visited nodes
        if node_id in visited:
            continue
        visited.add(node_id)

        # Yield if predicate matches (or no predicate)
        if predicate is None or predicate(current):
            yield current

        # Push children if within depth limit
        if max_depth is None or depth < max_depth:
            # Push children in reverse order for DFS ordering
            children = list(current.children())
            for child in reversed(children):
                stack.append((child, depth + 1))

Configuration

Classes for controlling inspection behavior.

Classes:

Name Description
InspectConfig

Configuration for type introspection.

EvalMode

How to evaluate annotations during introspection.

typing_graph.InspectConfig dataclass

Attributes:

Name Type Description
eval_mode EvalMode

How to evaluate annotations (default: DEFERRED).

globalns dict[str, Any] | None

Global namespace for forward reference resolution.

localns dict[str, Any] | None

Local namespace for forward reference resolution.

auto_namespace bool

Automatically extract namespaces from source objects (classes, functions, modules) for forward reference resolution. When True, namespaces are extracted from the inspected object and merged with user-provided globalns/localns (user values take precedence). Disable by setting to False for explicit namespace control only. Default: True.

max_depth int | None

Maximum recursion depth (None = unlimited).

include_private bool

Include private members starting with underscore.

include_inherited bool

Include inherited members from base classes.

include_methods bool

Include methods in class inspection.

include_class_vars bool

Include ClassVar annotations.

include_instance_vars bool

Include instance variable annotations.

hoist_metadata bool

Move Annotated metadata to node.metadata.

normalize_unions bool

Represent all union types as UnionNode regardless of runtime form. When True (default), both types.UnionType and typing.Union produce UnionNode, matching Python 3.14 behavior. Set to False to preserve native runtime representation. Use :func:~typing_graph.is_union_node to check if a node represents a union regardless of the normalization setting.

include_source_locations bool

Track source file and line numbers (disabled by default as inspect.getsourcelines is expensive).

Methods:

Name Description
get_format

Map eval_mode to typing_extensions.Format.

Source code in src/typing_graph/_config.py
@dataclass(slots=True, frozen=True)
class InspectConfig:
    """Configuration for type introspection.

    Attributes:
        eval_mode: How to evaluate annotations (default: DEFERRED).
        globalns: Global namespace for forward reference resolution.
        localns: Local namespace for forward reference resolution.
        auto_namespace: Automatically extract namespaces from source objects
            (classes, functions, modules) for forward reference resolution.
            When True, namespaces are extracted from the inspected object and
            merged with user-provided globalns/localns (user values take
            precedence). Disable by setting to False for explicit namespace
            control only. Default: True.
        max_depth: Maximum recursion depth (None = unlimited).
        include_private: Include private members starting with underscore.
        include_inherited: Include inherited members from base classes.
        include_methods: Include methods in class inspection.
        include_class_vars: Include ClassVar annotations.
        include_instance_vars: Include instance variable annotations.
        hoist_metadata: Move Annotated metadata to node.metadata.
        normalize_unions: Represent all union types as UnionNode regardless
            of runtime form. When True (default), both types.UnionType and
            typing.Union produce UnionNode, matching Python 3.14 behavior.
            Set to False to preserve native runtime representation. Use
            :func:`~typing_graph.is_union_node` to check if a node represents
            a union regardless of the normalization setting.
        include_source_locations: Track source file and line numbers
            (disabled by default as inspect.getsourcelines is expensive).

    Note:
        **Cache behavior:** Only the default ``InspectConfig()`` singleton uses
        the global inspection cache. Creating a custom config instance (even with
        identical values) bypasses the cache entirely. This ensures configuration
        isolation but may impact performance for repeated inspections with custom
        configs. If you need caching with custom settings, consider reusing the
        same config instance across calls.
    """

    # Annotation evaluation strategy
    eval_mode: EvalMode = EvalMode.DEFERRED

    # Namespaces for resolution
    globalns: dict[str, Any] | None = None
    localns: dict[str, Any] | None = None
    auto_namespace: bool = True  # Auto-extract namespaces from source objects

    # Recursion control
    max_depth: int | None = None  # None = unlimited

    # Class inspection options
    include_private: bool = False
    include_inherited: bool = True
    include_methods: bool = True
    include_class_vars: bool = True
    include_instance_vars: bool = True

    # Annotated handling
    hoist_metadata: bool = True  # Move Annotated metadata to node.metadata

    # Union normalization
    normalize_unions: bool = True  # All unions → UnionNode (matches Python 3.14)

    # Source tracking (disabled by default - inspect.getsourcelines is expensive)
    include_source_locations: bool = False

    def get_format(self) -> Format:
        """Map eval_mode to typing_extensions.Format.

        Returns:
            The Format corresponding to the current eval_mode.
        """
        return EVAL_MODE_TO_FORMAT[self.eval_mode]

get_format

get_format() -> Format

Returns:

Type Description
Format

The Format corresponding to the current eval_mode.

Source code in src/typing_graph/_config.py
def get_format(self) -> Format:
    """Map eval_mode to typing_extensions.Format.

    Returns:
        The Format corresponding to the current eval_mode.
    """
    return EVAL_MODE_TO_FORMAT[self.eval_mode]

typing_graph.EvalMode

Bases: Enum

Attributes:

Name Type Description
EAGER

Fully resolve annotations, fail on errors.

DEFERRED

Use ForwardRef for unresolvable annotations (default).

STRINGIFIED

Keep annotations as strings, resolve lazily.

Source code in src/typing_graph/_config.py
class EvalMode(Enum):
    """How to evaluate annotations during introspection.

    Attributes:
        EAGER: Fully resolve annotations, fail on errors.
        DEFERRED: Use ForwardRef for unresolvable annotations (default).
        STRINGIFIED: Keep annotations as strings, resolve lazily.
    """

    EAGER = auto()
    DEFERRED = auto()
    STRINGIFIED = auto()

Namespace extraction

Functions for extracting global and local namespaces from Python objects. These functions support forward reference resolution by providing namespace context from classes, functions, and modules.

For practical usage guidance, see How to configure namespaces. For background on why namespace extraction matters, see Forward references.

Type aliases

Attributes:

Name Type Description
NamespacePair TypeAlias

Type alias for the (globalns, localns) tuple returned by extraction functions.

NamespaceSource TypeAlias

Type alias for valid namespace extraction sources (class, function, or module).

typing_graph.NamespacePair module-attribute

NamespacePair: TypeAlias = tuple[dict[str, Any], dict[str, Any]]

typing_graph.NamespaceSource module-attribute

NamespaceSource: TypeAlias = 'type[Any] | Callable[..., Any] | ModuleType'

Extraction functions

Functions:

Name Description
extract_namespace

Extract namespaces from any valid source object.

extract_class_namespace

Extract global and local namespaces from a class.

extract_function_namespace

Extract global and local namespaces from a function.

extract_module_namespace

Extract global and local namespaces from a module.

typing_graph.extract_namespace

extract_namespace(source: NamespaceSource) -> NamespacePair

Parameters:

Name Type Description Default
source NamespaceSource

A class, function, or module.

required

Returns:

Type Description
NamespacePair

A tuple of (globalns, localns) dicts.

Raises:

Type Description
TypeError

If source is not a class, callable, or module.

Source code in src/typing_graph/_namespace.py
def extract_namespace(source: NamespaceSource) -> NamespacePair:
    """Extract namespaces from any valid source object.

    Dispatches to the appropriate extraction function based on source type:

    - For classes: uses ``extract_class_namespace``
    - For callables (functions/methods): uses ``extract_function_namespace``
    - For modules: uses ``extract_module_namespace``

    Args:
        source: A class, function, or module.

    Returns:
        A tuple of (globalns, localns) dicts.

    Raises:
        TypeError: If source is not a class, callable, or module.
    """
    if isinstance(source, type):
        return extract_class_namespace(source)

    if isinstance(source, ModuleType):
        return extract_module_namespace(source)

    if callable(source):
        return extract_function_namespace(source)

    # Pyright correctly identifies this as unreachable based on the type signature,
    # but we keep this guard for runtime safety when called from untyped code
    msg = f"source must be a class, callable, or module, got {type(source).__name__!r}"  # pyright: ignore[reportUnreachable]
    raise TypeError(msg)

typing_graph.extract_class_namespace

extract_class_namespace(cls: type[Any]) -> NamespacePair

Parameters:

Name Type Description Default
cls type[Any]

The class to extract namespaces from.

required

Returns:

Type Description
NamespacePair

A tuple of (globalns, localns) dicts. The globalns is a copy of the

NamespacePair

module's namespace; the localns is a new dict containing the class

NamespacePair

and any type parameters.

Source code in src/typing_graph/_namespace.py
def extract_class_namespace(cls: type[Any]) -> NamespacePair:
    """Extract global and local namespaces from a class.

    The global namespace is extracted from the class's defining module via
    ``sys.modules[cls.__module__].__dict__``. The local namespace includes
    the class itself under its ``__name__`` to enable self-referential type
    resolution, plus any type parameters from ``__type_params__`` (PEP 695).

    Args:
        cls: The class to extract namespaces from.

    Returns:
        A tuple of (globalns, localns) dicts. The globalns is a copy of the
        module's namespace; the localns is a new dict containing the class
        and any type parameters.
    """
    globalns: dict[str, Any] = {}
    localns: dict[str, Any] = {}

    # Extract global namespace from module
    module_name = getattr(cls, "__module__", None)
    if module_name is not None:
        module = sys.modules.get(module_name)
        if module is not None:
            module_dict = getattr(module, "__dict__", None)
            if isinstance(module_dict, dict):
                globalns = dict(module_dict)

    # Add class itself to local namespace for self-referential resolution
    class_name = getattr(cls, "__name__", None)
    if class_name is not None:
        localns[class_name] = cls

    # Add type parameters (PEP 695, Python 3.12+)
    _add_type_params_to_namespace(cls, localns)

    return globalns, localns

typing_graph.extract_function_namespace

extract_function_namespace(func: Callable[..., Any]) -> NamespacePair

Parameters:

Name Type Description Default
func Callable[..., Any]

The function to extract namespaces from.

required

Returns:

Type Description
NamespacePair

A tuple of (globalns, localns) dicts. The globalns is a copy of the

NamespacePair

function's globals; the localns is a new dict containing the owning

NamespacePair

class (if a method) and any type parameters.

Source code in src/typing_graph/_namespace.py
def extract_function_namespace(
    func: "Callable[..., Any]",
) -> NamespacePair:
    """Extract global and local namespaces from a function.

    The global namespace is extracted from ``func.__globals__``. For methods,
    the system attempts to resolve the owning class via ``__qualname__`` parsing
    and includes it in the local namespace if found. Type parameters from
    ``__type_params__`` (PEP 695) are also included in the local namespace.

    Args:
        func: The function to extract namespaces from.

    Returns:
        A tuple of (globalns, localns) dicts. The globalns is a copy of the
        function's globals; the localns is a new dict containing the owning
        class (if a method) and any type parameters.
    """
    globalns: dict[str, Any] = {}
    localns: dict[str, Any] = {}

    # Extract global namespace from __globals__
    func_globals = getattr(func, "__globals__", None)
    if isinstance(func_globals, dict):
        globalns = dict(func_globals)

    # Attempt to resolve owning class for methods
    owning_class = _resolve_owning_class(func)
    if owning_class is not None:
        class_name = getattr(owning_class, "__name__", None)
        if class_name is not None:
            localns[class_name] = owning_class

    # Add type parameters (PEP 695, Python 3.12+)
    _add_type_params_to_namespace(func, localns)

    return globalns, localns

typing_graph.extract_module_namespace

extract_module_namespace(module: ModuleType) -> NamespacePair

Parameters:

Name Type Description Default
module ModuleType

The module to extract namespaces from.

required

Returns:

Type Description
NamespacePair

A tuple of (globalns, localns) dicts. The globalns is a copy of the

NamespacePair

module's namespace; the localns is always an empty dict for modules.

Source code in src/typing_graph/_namespace.py
def extract_module_namespace(
    module: ModuleType,
) -> NamespacePair:
    """Extract global and local namespaces from a module.

    The global namespace is the module's ``__dict__``. The local namespace
    is always empty for modules.

    Args:
        module: The module to extract namespaces from.

    Returns:
        A tuple of (globalns, localns) dicts. The globalns is a copy of the
        module's namespace; the localns is always an empty dict for modules.
    """
    globalns: dict[str, Any] = {}
    localns: dict[str, Any] = {}

    module_dict = getattr(module, "__dict__", None)
    if isinstance(module_dict, dict):
        globalns = dict(module_dict)

    return globalns, localns

Type nodes

Base classes

The foundational type node class and type variable.

Classes:

Name Description
TypeNode

Base class for all type graph nodes.

Attributes:

Name Type Description
TypeNodeT

TypeVar bound to TypeNode for generic functions over node types.

typing_graph.TypeNodeT module-attribute

TypeNodeT = TypeVar('TypeNodeT', bound=TypeNode)

typing_graph.TypeNode dataclass

Bases: ABC

Attributes:

Name Type Description
source SourceLocation | None

Optional source location where this type was defined.

metadata MetadataCollection

Metadata extracted from an enclosing Annotated[T, ...]. When a type is wrapped in Annotated, the metadata can be hoisted to this field during graph construction, allowing the Annotated wrapper to be elided while preserving the metadata.

qualifiers frozenset[Qualifier]

Type qualifiers (ClassVar, Final, Required, NotRequired, ReadOnly, InitVar) extracted from the annotation. Uses the same qualifier type as typing_inspection.

Methods:

Name Description
children

Return child type nodes for graph traversal.

edges

Return all outgoing edges from this node.

resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class TypeNode(ABC):
    """Base class for all type graph nodes.

    Attributes:
        source: Optional source location where this type was defined.
        metadata: Metadata extracted from an enclosing Annotated[T, ...].
            When a type is wrapped in Annotated, the metadata can be hoisted
            to this field during graph construction, allowing the Annotated
            wrapper to be elided while preserving the metadata.
        qualifiers: Type qualifiers (ClassVar, Final, Required, NotRequired,
            ReadOnly, InitVar) extracted from the annotation. Uses the same
            qualifier type as typing_inspection.
    """

    source: SourceLocation | None = field(default=None, kw_only=True)
    metadata: MetadataCollection = field(
        default_factory=lambda: MetadataCollection.EMPTY, kw_only=True
    )
    qualifiers: "frozenset[Qualifier]" = field(default_factory=frozenset, kw_only=True)

    @abstractmethod
    def children(self) -> "Sequence[TypeNode]":
        """Return child type nodes for graph traversal.

        This method provides a faster traversal path when edge metadata
        is not required.
        """
        ...

    @abstractmethod
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        """Return all outgoing edges from this node.

        Forward reference handling: This method MUST NOT trigger forward
        reference resolution. Forward references may cause import cycles
        or execution of arbitrary code. Implementations SHOULD return
        edges to ForwardRefNode instances without resolving them.

        ForwardRefNode behavior: When a ForwardRefNode is unresolved or
        resolution failed, edges() MUST return an empty sequence (no
        RESOLVED edge). Only successfully resolved forward references
        produce a RESOLVED edge to the target node.
        """
        ...

    def resolved(self) -> "TypeNode":
        """Return the terminal resolved type, traversing forward reference chains.

        For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
        with RefResolved state, traverses the chain to find the terminal
        non-ForwardRefNode type. For unresolvable references (RefUnresolved,
        RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

        This method only traverses existing RefResolved chains - it does NOT
        trigger resolution of RefUnresolved forward references. To resolve
        forward references, use the graph construction APIs with appropriate
        namespace configuration.

        Returns:
            The terminal resolved TypeNode, or self if this is already a
            concrete (non-ForwardRefNode) type.

        Note:
            ForwardRefNode overrides this method to implement chain traversal.
            To distinguish why self was returned from a ForwardRefNode, check
            the node type and state:

            ```python
            result = node.resolved()
            if result is node and isinstance(node, ForwardRefNode):
                if isinstance(node.state, RefUnresolved):
                    # Resolution was never attempted
                    ...
                elif isinstance(node.state, RefFailed):
                    # Resolution failed: node.state.error has details
                    ...
                else:
                    # Cycle detected (RefResolved but returned self)
                    ...
            ```

            Prefer the method form for chaining: ``node.resolved().children()``.
            Prefer the function form for map/filter:
            ``map(resolve_forward_ref, nodes)``.

        Example:
            Chained usage for immediate attribute access:

            >>> from typing_graph import ConcreteNode
            >>> node = ConcreteNode(cls=int)
            >>> node.resolved() is node
            True
            >>> node.resolved().children()
            ()
        """
        return self
children abstractmethod
children() -> Sequence[TypeNode]
Source code in src/typing_graph/_node.py
@abstractmethod
def children(self) -> "Sequence[TypeNode]":
    """Return child type nodes for graph traversal.

    This method provides a faster traversal path when edge metadata
    is not required.
    """
    ...
edges abstractmethod
Source code in src/typing_graph/_node.py
@abstractmethod
def edges(self) -> "Sequence[TypeEdgeConnection]":
    """Return all outgoing edges from this node.

    Forward reference handling: This method MUST NOT trigger forward
    reference resolution. Forward references may cause import cycles
    or execution of arbitrary code. Implementations SHOULD return
    edges to ForwardRefNode instances without resolving them.

    ForwardRefNode behavior: When a ForwardRefNode is unresolved or
    resolution failed, edges() MUST return an empty sequence (no
    RESOLVED edge). Only successfully resolved forward references
    produce a RESOLVED edge to the target node.
    """
    ...
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

Concrete and generic types

Nodes representing concrete types, generics, and annotated types.

Classes:

Name Description
ConcreteNode

A non-generic nominal type: int, str, MyClass, etc.

SubscriptedGenericNode

Generic with type args applied: List[int], Dict[str, T], etc.

GenericAliasNode

Parameterized type alias: type Vector[T] = list[T] (PEP 695).

GenericTypeNode

An unsubscripted generic (type constructor): list, Dict, etc.

AnnotatedNode

Annotated[T, metadata, ...].

NewTypeNode

NewType('Name', base) - a distinct type alias for type checking.

TypeAliasNode

typing.TypeAlias or PEP 695 type statement runtime object.

typing_graph.ConcreteNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class ConcreteNode(TypeNode):
    """A non-generic nominal type: int, str, MyClass, etc."""

    cls: type

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return ()

    @override
    def children(self) -> "Sequence[TypeNode]":
        return ()

    @override
    def __str__(self) -> str:
        return self.cls.__name__
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.SubscriptedGenericNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class SubscriptedGenericNode(TypeNode):
    """Generic with type args applied: List[int], Dict[str, T], etc."""

    origin: TypeNode  # GenericType or another SubscriptedGenericNode
    args: tuple[TypeNode, ...]
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (self.origin, *self.args))
        edges: list[TypeEdgeConnection] = [
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.ORIGIN), self.origin)
        ]
        edges.extend(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.TYPE_ARG, index=i), arg)
            for i, arg in enumerate(self.args)
        )
        object.__setattr__(self, "_edges", tuple(edges))

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children

    @override
    def __str__(self) -> str:
        args_str = ", ".join(str(arg) for arg in self.args)
        return f"{self.origin}[{args_str}]"
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.GenericAliasNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class GenericAliasNode(TypeNode):
    """Parameterized type alias: type Vector[T] = list[T] (PEP 695)."""

    name: str
    type_params: tuple[TypeVarNode | ParamSpecNode | TypeVarTupleNode, ...]
    value: TypeNode  # The aliased type (may reference type_params)
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (*self.type_params, self.value))
        edges: list[TypeEdgeConnection] = [
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.TYPE_PARAM, index=i), tp)
            for i, tp in enumerate(self.type_params)
        ]
        edges.append(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.ALIAS_TARGET), self.value)
        )
        object.__setattr__(self, "_edges", tuple(edges))

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.GenericTypeNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class GenericTypeNode(TypeNode):
    """An unsubscripted generic (type constructor): list, Dict, etc."""

    cls: type
    type_params: tuple[TypeVarNode | ParamSpecNode | TypeVarTupleNode, ...] = ()
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        edges = tuple(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.TYPE_PARAM, index=i), tp)
            for i, tp in enumerate(self.type_params)
        )
        object.__setattr__(self, "_edges", edges)

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self.type_params

    @override
    def __str__(self) -> str:
        return self.cls.__name__
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.AnnotatedNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class AnnotatedNode(TypeNode):
    """Annotated[T, metadata, ...].

    Note: During graph construction, you may choose to hoist metadata to the
    inner type's `metadata` field and elide the AnnotatedNode wrapper. This
    node represents the un-elided form.
    """

    base: TypeNode
    # Note: metadata is on base TypeNode, but annotations here are the raw
    # Annotated arguments, which may include type-system extensions
    annotations: tuple[object, ...] = ()
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (self.base,))
        object.__setattr__(
            self,
            "_edges",
            (TypeEdgeConnection(TypeEdge(TypeEdgeKind.ANNOTATED_BASE), self.base),),
        )

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.NewTypeNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class NewTypeNode(TypeNode):
    """NewType('Name', base) - a distinct type alias for type checking."""

    name: str
    supertype: TypeNode
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (self.supertype,))
        object.__setattr__(
            self,
            "_edges",
            (TypeEdgeConnection(TypeEdge(TypeEdgeKind.SUPERTYPE), self.supertype),),
        )

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.TypeAliasNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class TypeAliasNode(TypeNode):
    """typing.TypeAlias or PEP 695 type statement runtime object."""

    name: str
    value: TypeNode
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (self.value,))
        object.__setattr__(
            self,
            "_edges",
            (TypeEdgeConnection(TypeEdge(TypeEdgeKind.ALIAS_TARGET), self.value),),
        )

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

Union and intersection types

Nodes representing type unions and intersections.

Classes:

Name Description
UnionNode

A | B union type.

IntersectionNode

Intersection of types (not yet in typing, but used by type checkers).

typing_graph.UnionNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class UnionNode(TypeNode):
    """A | B union type."""

    members: tuple[TypeNode, ...]
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        edges = tuple(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.UNION_MEMBER, index=i), m)
            for i, m in enumerate(self.members)
        )
        object.__setattr__(self, "_edges", edges)

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self.members

    @override
    def __str__(self) -> str:
        return " | ".join(str(m) for m in self.members)
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.IntersectionNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class IntersectionNode(TypeNode):
    """Intersection of types (not yet in typing, but used by type checkers)."""

    members: tuple[TypeNode, ...]
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        edges = tuple(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.INTERSECTION_MEMBER, index=i), m)
            for i, m in enumerate(self.members)
        )
        object.__setattr__(self, "_edges", edges)

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self.members
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

Type variables

Nodes representing type parameters and variance.

Classes:

Name Description
TypeVarNode

A TypeVar - placeholder for a single type.

ParamSpecNode

A ParamSpec - placeholder for callable parameter lists.

TypeVarTupleNode

A TypeVarTuple - placeholder for variadic type args (PEP 646).

Variance

Variance of a type variable.

Attributes:

Name Type Description
TypeParamNode

Type alias for nodes representing type parameters.

typing_graph.TypeParamNode module-attribute

typing_graph.TypeVarNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class TypeVarNode(TypeNode):
    """A TypeVar - placeholder for a single type.

    Example:
        T = TypeVar('T')
        T = TypeVar('T', bound=int)
        T = TypeVar('T', int, str)  # constraints
    """

    name: str
    variance: Variance = Variance.INVARIANT
    bound: TypeNode | None = None
    constraints: tuple[TypeNode, ...] = ()
    default: TypeNode | None = None  # PEP 696
    infer_variance: bool = False  # PEP 695 auto-variance
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        children: list[TypeNode] = list(self.constraints)
        edges: list[TypeEdgeConnection] = [
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.CONSTRAINT, index=i), c)
            for i, c in enumerate(self.constraints)
        ]
        if self.bound:
            children.append(self.bound)
            edges.append(TypeEdgeConnection(TypeEdge(TypeEdgeKind.BOUND), self.bound))
        if self.default:
            children.append(self.default)
            edges.append(
                TypeEdgeConnection(TypeEdge(TypeEdgeKind.DEFAULT), self.default)
            )
        object.__setattr__(self, "_children", tuple(children))
        object.__setattr__(self, "_edges", tuple(edges))

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.ParamSpecNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class ParamSpecNode(TypeNode):
    """A ParamSpec - placeholder for callable parameter lists.

    Example:
        P = ParamSpec('P')
        def decorator(f: Callable[P, R]) -> Callable[P, R]: ...
    """

    name: str
    default: TypeNode | None = None  # PEP 696
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        if self.default:
            children: tuple[TypeNode, ...] = (self.default,)
            edges = (TypeEdgeConnection(TypeEdge(TypeEdgeKind.DEFAULT), self.default),)
        else:
            children = ()
            edges = ()
        object.__setattr__(self, "_children", children)
        object.__setattr__(self, "_edges", edges)

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.TypeVarTupleNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class TypeVarTupleNode(TypeNode):
    """A TypeVarTuple - placeholder for variadic type args (PEP 646).

    Example:
        Ts = TypeVarTuple('Ts')
        def f(*args: *Ts) -> tuple[*Ts]: ...
    """

    name: str
    default: TypeNode | None = None  # PEP 696
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        if self.default:
            children: tuple[TypeNode, ...] = (self.default,)
            edges = (TypeEdgeConnection(TypeEdge(TypeEdgeKind.DEFAULT), self.default),)
        else:
            children = ()
            edges = ()
        object.__setattr__(self, "_children", children)
        object.__setattr__(self, "_edges", edges)

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.Variance

Bases: Enum

Source code in src/typing_graph/_node.py
class Variance(Enum):
    """Variance of a type variable."""

    INVARIANT = auto()
    COVARIANT = auto()
    CONTRAVARIANT = auto()

Special types

Nodes for special typing constructs.

Classes:

Name Description
AnyNode

typing.Any - compatible with all types (gradual typing escape hatch).

NeverNode

typing.Never / typing.NoReturn - the bottom type (uninhabited).

SelfNode

typing.Self - reference to the enclosing class.

LiteralNode

Literal[v1, v2, ...] - specific literal values as types.

LiteralStringNode

typing.LiteralString - any literal string value (PEP 675).

TupleNode

Tuple types in various forms.

EllipsisNode

The ... used in Callable[..., R] and Tuple[T, ...].

ForwardRefNode

A string forward reference like 'MyClass'.

MetaNode

Type[T] or type[T] - the class object itself, not an instance.

ConcatenateNode

Concatenate[X, Y, P] - prepend args to a ParamSpec (PEP 612).

UnpackNode

Unpack[Ts] or *Ts - unpack a TypeVarTuple (PEP 646).

TypeGuardNode

typing.TypeGuard[T] - narrows type in true branch (PEP 647).

TypeIsNode

typing.TypeIs[T] - narrows type bidirectionally (PEP 742).

typing_graph.AnyNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class AnyNode(TypeNode):
    """typing.Any - compatible with all types (gradual typing escape hatch)."""

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return ()

    @override
    def children(self) -> "Sequence[TypeNode]":
        return ()
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.NeverNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class NeverNode(TypeNode):
    """typing.Never / typing.NoReturn - the bottom type (uninhabited)."""

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return ()

    @override
    def children(self) -> "Sequence[TypeNode]":
        return ()
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.SelfNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class SelfNode(TypeNode):
    """typing.Self - reference to the enclosing class."""

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return ()

    @override
    def children(self) -> "Sequence[TypeNode]":
        return ()
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.LiteralNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class LiteralNode(TypeNode):
    """Literal[v1, v2, ...] - specific literal values as types."""

    values: tuple[object, ...]

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return ()

    @override
    def children(self) -> "Sequence[TypeNode]":
        return ()
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.LiteralStringNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class LiteralStringNode(TypeNode):
    """typing.LiteralString - any literal string value (PEP 675)."""

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return ()

    @override
    def children(self) -> "Sequence[TypeNode]":
        return ()
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.TupleNode dataclass

Bases: TypeNode

Examples:

tuple[int, str] - heterogeneous (elements=(int, str), homogeneous=False) tuple[int, ...] - homogeneous (elements=(int,), homogeneous=True) tuple[int, *Ts, str] - variadic (contains UnpackNode) tuple[()] - empty tuple (elements=(), homogeneous=False)

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class TupleNode(TypeNode):
    """Tuple types in various forms.

    Examples:
        tuple[int, str]      - heterogeneous (elements=(int, str), homogeneous=False)
        tuple[int, ...]      - homogeneous (elements=(int,), homogeneous=True)
        tuple[int, *Ts, str] - variadic (contains UnpackNode)
        tuple[()]            - empty tuple (elements=(), homogeneous=False)
    """

    elements: tuple[TypeNode, ...]
    homogeneous: bool = False  # True for tuple[T, ...]
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        edges = tuple(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.ELEMENT, index=i), e)
            for i, e in enumerate(self.elements)
        )
        object.__setattr__(self, "_edges", edges)

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self.elements

    @override
    def __str__(self) -> str:
        if self.homogeneous and self.elements:
            return f"tuple[{self.elements[0]}, ...]"
        if not self.elements:
            return "tuple[()]"
        return f"tuple[{', '.join(str(e) for e in self.elements)}]"
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.EllipsisNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class EllipsisNode(TypeNode):
    """The ... used in Callable[..., R] and Tuple[T, ...]."""

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return ()

    @override
    def children(self) -> "Sequence[TypeNode]":
        return ()
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.ForwardRefNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class ForwardRefNode(TypeNode):
    """A string forward reference like 'MyClass'."""

    ref: str
    state: RefState = field(default_factory=RefUnresolved)
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        if isinstance(self.state, RefResolved):
            children: tuple[TypeNode, ...] = (self.state.node,)
            edges = (
                TypeEdgeConnection(TypeEdge(TypeEdgeKind.RESOLVED), self.state.node),
            )
        else:
            children = ()
            edges = ()
        object.__setattr__(self, "_children", children)
        object.__setattr__(self, "_edges", edges)

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children

    @override
    def __str__(self) -> str:
        if isinstance(self.state, RefResolved):
            return f"ForwardRef({self.ref!r}) -> {self.state.node}"
        if isinstance(self.state, RefFailed):
            return f"ForwardRef({self.ref!r}) [failed: {self.state.error}]"
        return f"ForwardRef({self.ref!r})"

    @override
    def resolved(self) -> "TypeNode":
        """Return the terminal resolved type, traversing forward reference chains.

        Traverses chains of resolved forward references until reaching either:
        - A non-ForwardRefNode type (the terminal resolution)
        - An unresolvable ForwardRefNode (RefUnresolved or RefFailed state)
        - A cycle (detected via identity tracking)

        This method only traverses existing RefResolved chains - it does NOT
        trigger resolution of RefUnresolved forward references. To resolve
        forward references, use the graph construction APIs with appropriate
        namespace configuration.

        Returns:
            The terminal resolved TypeNode, or self if unresolvable.

        Note:
            When self is returned, the reason can be determined by checking:
            - RefUnresolved state: resolution was never attempted
            - RefFailed state: resolution failed (check state.error)
            - RefResolved state: cycle detected in reference chain

        Example:
            Single resolution step:

            >>> from typing_graph import ConcreteNode, ForwardRefNode, RefResolved
            >>> target = ConcreteNode(cls=int)
            >>> ref = ForwardRefNode(ref="int", state=RefResolved(node=target))
            >>> ref.resolved() is target
            True

            Chain traversal:

            >>> inner = ForwardRefNode(ref="int", state=RefResolved(node=target))
            >>> outer = ForwardRefNode(ref="Inner", state=RefResolved(node=inner))
            >>> outer.resolved() is target
            True
        """
        seen: set[int] = set()
        current: TypeNode = self
        while isinstance(current, ForwardRefNode):
            node_id = id(current)
            if node_id in seen:
                return current  # Cycle detected
            seen.add(node_id)
            if isinstance(current.state, RefResolved):
                current = current.state.node
            else:
                return current  # Unresolvable (RefUnresolved or RefFailed)
        return current
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if unresolvable.

Source code in src/typing_graph/_node.py
@override
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    Traverses chains of resolved forward references until reaching either:
    - A non-ForwardRefNode type (the terminal resolution)
    - An unresolvable ForwardRefNode (RefUnresolved or RefFailed state)
    - A cycle (detected via identity tracking)

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if unresolvable.

    Note:
        When self is returned, the reason can be determined by checking:
        - RefUnresolved state: resolution was never attempted
        - RefFailed state: resolution failed (check state.error)
        - RefResolved state: cycle detected in reference chain

    Example:
        Single resolution step:

        >>> from typing_graph import ConcreteNode, ForwardRefNode, RefResolved
        >>> target = ConcreteNode(cls=int)
        >>> ref = ForwardRefNode(ref="int", state=RefResolved(node=target))
        >>> ref.resolved() is target
        True

        Chain traversal:

        >>> inner = ForwardRefNode(ref="int", state=RefResolved(node=target))
        >>> outer = ForwardRefNode(ref="Inner", state=RefResolved(node=inner))
        >>> outer.resolved() is target
        True
    """
    seen: set[int] = set()
    current: TypeNode = self
    while isinstance(current, ForwardRefNode):
        node_id = id(current)
        if node_id in seen:
            return current  # Cycle detected
        seen.add(node_id)
        if isinstance(current.state, RefResolved):
            current = current.state.node
        else:
            return current  # Unresolvable (RefUnresolved or RefFailed)
    return current

typing_graph.MetaNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class MetaNode(TypeNode):
    """Type[T] or type[T] - the class object itself, not an instance."""

    of: TypeNode
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (self.of,))
        object.__setattr__(
            self,
            "_edges",
            (TypeEdgeConnection(TypeEdge(TypeEdgeKind.META_OF), self.of),),
        )

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.ConcatenateNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class ConcatenateNode(TypeNode):
    """Concatenate[X, Y, P] - prepend args to a ParamSpec (PEP 612).

    Example:
        Callable[Concatenate[int, str, P], R]
    """

    prefix: tuple[TypeNode, ...]
    param_spec: ParamSpecNode
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (*self.prefix, self.param_spec))
        edges: list[TypeEdgeConnection] = [
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.PREFIX, index=i), p)
            for i, p in enumerate(self.prefix)
        ]
        edges.append(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.PARAM_SPEC), self.param_spec)
        )
        object.__setattr__(self, "_edges", tuple(edges))

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.UnpackNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class UnpackNode(TypeNode):
    """Unpack[Ts] or *Ts - unpack a TypeVarTuple (PEP 646).

    Example:
        def f(*args: *Ts) -> tuple[*Ts]: ...
        tuple[int, *Ts, str]
    """

    target: TypeVarTupleNode | TypeNode  # TypeVarTuple or a tuple type
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (self.target,))
        object.__setattr__(
            self,
            "_edges",
            (TypeEdgeConnection(TypeEdge(TypeEdgeKind.TARGET), self.target),),
        )

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.TypeGuardNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class TypeGuardNode(TypeNode):
    """typing.TypeGuard[T] - narrows type in true branch (PEP 647)."""

    narrows_to: TypeNode
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (self.narrows_to,))
        object.__setattr__(
            self,
            "_edges",
            (TypeEdgeConnection(TypeEdge(TypeEdgeKind.NARROWS), self.narrows_to),),
        )

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.TypeIsNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class TypeIsNode(TypeNode):
    """typing.TypeIs[T] - narrows type bidirectionally (PEP 742)."""

    narrows_to: TypeNode
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (self.narrows_to,))
        object.__setattr__(
            self,
            "_edges",
            (TypeEdgeConnection(TypeEdge(TypeEdgeKind.NARROWS), self.narrows_to),),
        )

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children
resolved
resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

Forward reference state

Types representing forward reference resolution states.

Classes:

Name Description
RefResolved

Successfully resolved to a type.

RefUnresolved

Not yet attempted to resolve.

RefFailed

Resolution attempted but failed.

Attributes:

Name Type Description
RefState

Type alias for forward reference resolution states.

typing_graph.RefState module-attribute

typing_graph.RefResolved dataclass

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class RefResolved:
    """Successfully resolved to a type."""

    node: TypeNode

typing_graph.RefUnresolved dataclass

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class RefUnresolved:
    """Not yet attempted to resolve."""

typing_graph.RefFailed dataclass

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class RefFailed:
    """Resolution attempted but failed."""

    error: str

Structured type nodes

Nodes representing classes, dataclasses, TypedDicts, and other structured types.

Classes:

Name Description
StructuredNode

Base for types with named, typed fields.

ClassNode

A class with full type information.

DataclassNode

A dataclass with typed fields and configuration.

TypedDictNode

TypedDict with named fields.

NamedTupleNode

NamedTuple with named fields.

EnumNode

An Enum with typed members.

ProtocolNode

Protocol defining structural interface.

typing_graph.StructuredNode dataclass

Bases: TypeNode, ABC

Methods:

Name Description
children

Return child type nodes for graph traversal.

edges

Return all outgoing edges from this node.

resolved

Return the terminal resolved type, traversing forward reference chains.

get_fields

Return the field definitions.

Source code in src/typing_graph/_node.py
class StructuredNode(TypeNode, ABC):
    """Base for types with named, typed fields."""

    @abstractmethod
    def get_fields(self) -> tuple[FieldDef, ...]:
        """Return the field definitions."""
        ...

children abstractmethod

children() -> Sequence[TypeNode]
Source code in src/typing_graph/_node.py
@abstractmethod
def children(self) -> "Sequence[TypeNode]":
    """Return child type nodes for graph traversal.

    This method provides a faster traversal path when edge metadata
    is not required.
    """
    ...

edges abstractmethod

Source code in src/typing_graph/_node.py
@abstractmethod
def edges(self) -> "Sequence[TypeEdgeConnection]":
    """Return all outgoing edges from this node.

    Forward reference handling: This method MUST NOT trigger forward
    reference resolution. Forward references may cause import cycles
    or execution of arbitrary code. Implementations SHOULD return
    edges to ForwardRefNode instances without resolving them.

    ForwardRefNode behavior: When a ForwardRefNode is unresolved or
    resolution failed, edges() MUST return an empty sequence (no
    RESOLVED edge). Only successfully resolved forward references
    produce a RESOLVED edge to the target node.
    """
    ...

resolved

resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

get_fields abstractmethod

get_fields() -> tuple[FieldDef, ...]
Source code in src/typing_graph/_node.py
@abstractmethod
def get_fields(self) -> tuple[FieldDef, ...]:
    """Return the field definitions."""
    ...

typing_graph.ClassNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class ClassNode(TypeNode):
    """A class with full type information.

    This is a meta-node representing the class definition itself, including
    its type parameters, bases, and members.
    """

    cls: type
    name: str
    type_params: tuple[TypeVarNode | ParamSpecNode | TypeVarTupleNode, ...] = ()
    bases: tuple[TypeNode, ...] = ()
    methods: tuple[MethodSig, ...] = ()
    class_vars: tuple[FieldDef, ...] = ()
    instance_vars: tuple[FieldDef, ...] = ()
    is_abstract: bool = False
    is_final: bool = False
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        children: list[TypeNode] = list(self.type_params)
        children.extend(self.bases)
        children.extend(mt.signature for mt in self.methods)
        children.extend(v.type for v in self.class_vars)
        children.extend(v.type for v in self.instance_vars)
        object.__setattr__(self, "_children", tuple(children))

        edges: list[TypeEdgeConnection] = [
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.TYPE_PARAM, index=i), tp)
            for i, tp in enumerate(self.type_params)
        ]
        edges.extend(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.BASE, index=i), base)
            for i, base in enumerate(self.bases)
        )
        edges.extend(
            TypeEdgeConnection(
                TypeEdge(TypeEdgeKind.METHOD, name=mt.name), mt.signature
            )
            for mt in self.methods
        )
        edges.extend(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.FIELD, name=v.name), v.type)
            for v in self.class_vars
        )
        edges.extend(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.FIELD, name=v.name), v.type)
            for v in self.instance_vars
        )
        object.__setattr__(self, "_edges", tuple(edges))

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children

resolved

resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.DataclassNode dataclass

Bases: StructuredNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class DataclassNode(StructuredNode):
    """A dataclass with typed fields and configuration."""

    cls: type
    fields: tuple[DataclassFieldDef, ...]
    frozen: bool = False
    slots: bool = False
    kw_only: bool = False
    match_args: bool = True
    order: bool = False
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", tuple(f.type for f in self.fields))
        edges = tuple(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.FIELD, name=f.name), f.type)
            for f in self.fields
        )
        object.__setattr__(self, "_edges", edges)

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def get_fields(self) -> tuple[FieldDef, ...]:
        return self.fields

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children

resolved

resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.TypedDictNode dataclass

Bases: StructuredNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class TypedDictNode(StructuredNode):
    """TypedDict with named fields."""

    name: str
    fields: tuple[FieldDef, ...]
    total: bool = True
    closed: bool = False  # PEP 728
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", tuple(f.type for f in self.fields))
        edges = tuple(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.FIELD, name=f.name), f.type)
            for f in self.fields
        )
        object.__setattr__(self, "_edges", edges)

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def get_fields(self) -> tuple[FieldDef, ...]:
        return self.fields

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children

resolved

resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.NamedTupleNode dataclass

Bases: StructuredNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class NamedTupleNode(StructuredNode):
    """NamedTuple with named fields."""

    name: str
    fields: tuple[FieldDef, ...]
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", tuple(f.type for f in self.fields))
        edges = tuple(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.FIELD, name=f.name), f.type)
            for f in self.fields
        )
        object.__setattr__(self, "_edges", edges)

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def get_fields(self) -> tuple[FieldDef, ...]:
        return self.fields

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children

resolved

resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.EnumNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class EnumNode(TypeNode):
    """An Enum with typed members."""

    cls: type
    value_type: TypeNode  # The type of enum values (int, str, etc.)
    members: tuple[tuple[str, object], ...]  # (name, value) pairs
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (self.value_type,))
        object.__setattr__(
            self,
            "_edges",
            (TypeEdgeConnection(TypeEdge(TypeEdgeKind.VALUE_TYPE), self.value_type),),
        )

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children

resolved

resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.ProtocolNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class ProtocolNode(TypeNode):
    """Protocol defining structural interface."""

    name: str
    methods: tuple[MethodSig, ...]
    attributes: tuple[FieldDef, ...] = ()
    is_runtime_checkable: bool = False
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        children: list[TypeNode] = [mt.signature for mt in self.methods]
        children.extend(a.type for a in self.attributes)
        object.__setattr__(self, "_children", tuple(children))

        edges: list[TypeEdgeConnection] = [
            TypeEdgeConnection(
                TypeEdge(TypeEdgeKind.METHOD, name=mt.name), mt.signature
            )
            for mt in self.methods
        ]
        edges.extend(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.FIELD, name=a.name), a.type)
            for a in self.attributes
        )
        object.__setattr__(self, "_edges", tuple(edges))

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children

resolved

resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

Function and callable nodes

Nodes representing functions, callables, and signatures.

Classes:

Name Description
FunctionNode

A function with full type information.

CallableNode

Callable[[P1, P2], R] or Callable[P, R] or Callable[..., R].

SignatureNode

A full callable signature with named parameters.

MethodSig

A method signature (not a TypeNode itself).

typing_graph.FunctionNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class FunctionNode(TypeNode):
    """A function with full type information.

    Use this for introspecting actual function definitions, not just
    callable type annotations.
    """

    name: str
    signature: SignatureNode
    is_async: bool = False
    is_generator: bool = False
    decorators: tuple[str, ...] = ()  # Decorator names for reference
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        object.__setattr__(self, "_children", (self.signature,))
        object.__setattr__(
            self,
            "_edges",
            (TypeEdgeConnection(TypeEdge(TypeEdgeKind.SIGNATURE), self.signature),),
        )

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children

resolved

resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.CallableNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class CallableNode(TypeNode):
    """Callable[[P1, P2], R] or Callable[P, R] or Callable[..., R]."""

    params: tuple[TypeNode, ...] | ParamSpecNode | ConcatenateNode | EllipsisNode
    returns: TypeNode
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        if isinstance(self.params, tuple):
            children = (*self.params, self.returns)
            # When params is a tuple, use indexed PARAM edges
            edges: list[TypeEdgeConnection] = [
                TypeEdgeConnection(TypeEdge(TypeEdgeKind.PARAM, index=i), p)
                for i, p in enumerate(self.params)
            ]
        else:
            children = (self.params, self.returns)
            # Single node (ParamSpec, Concatenate, Ellipsis) - no index
            edges = [TypeEdgeConnection(TypeEdge(TypeEdgeKind.PARAM), self.params)]
        edges.append(TypeEdgeConnection(TypeEdge(TypeEdgeKind.RETURN), self.returns))
        object.__setattr__(self, "_children", children)
        object.__setattr__(self, "_edges", tuple(edges))

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children

resolved

resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.SignatureNode dataclass

Bases: TypeNode

Methods:

Name Description
resolved

Return the terminal resolved type, traversing forward reference chains.

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class SignatureNode(TypeNode):
    """A full callable signature with named parameters.

    More detailed than CallableNode - includes parameter names, kinds, defaults.
    Use for introspecting actual functions/methods.
    """

    parameters: tuple[Parameter, ...]
    returns: TypeNode
    type_params: tuple[TypeVarNode | ParamSpecNode | TypeVarTupleNode, ...] = ()
    _children: tuple[TypeNode, ...] = field(
        init=False, repr=False, compare=False, hash=False
    )
    _edges: tuple["TypeEdgeConnection", ...] = field(
        init=False, repr=False, compare=False, hash=False
    )

    def __post_init__(self) -> None:
        children: list[TypeNode] = [p.type for p in self.parameters]
        children.append(self.returns)
        children.extend(self.type_params)
        object.__setattr__(self, "_children", tuple(children))

        edges: list[TypeEdgeConnection] = [
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.PARAM, name=p.name), p.type)
            for p in self.parameters
        ]
        edges.append(TypeEdgeConnection(TypeEdge(TypeEdgeKind.RETURN), self.returns))
        edges.extend(
            TypeEdgeConnection(TypeEdge(TypeEdgeKind.TYPE_PARAM, index=i), tp)
            for i, tp in enumerate(self.type_params)
        )
        object.__setattr__(self, "_edges", tuple(edges))

    @override
    def edges(self) -> "Sequence[TypeEdgeConnection]":
        return self._edges

    @override
    def children(self) -> "Sequence[TypeNode]":
        return self._children

resolved

resolved() -> TypeNode

Returns:

Type Description
TypeNode

The terminal resolved TypeNode, or self if this is already a

TypeNode

concrete (non-ForwardRefNode) type.

Source code in src/typing_graph/_node.py
def resolved(self) -> "TypeNode":
    """Return the terminal resolved type, traversing forward reference chains.

    For non-ForwardRefNode types, returns self unchanged. For ForwardRefNode
    with RefResolved state, traverses the chain to find the terminal
    non-ForwardRefNode type. For unresolvable references (RefUnresolved,
    RefFailed, or cycles), returns self (the unresolvable ForwardRefNode).

    This method only traverses existing RefResolved chains - it does NOT
    trigger resolution of RefUnresolved forward references. To resolve
    forward references, use the graph construction APIs with appropriate
    namespace configuration.

    Returns:
        The terminal resolved TypeNode, or self if this is already a
        concrete (non-ForwardRefNode) type.

    Note:
        ForwardRefNode overrides this method to implement chain traversal.
        To distinguish why self was returned from a ForwardRefNode, check
        the node type and state:

        ```python
        result = node.resolved()
        if result is node and isinstance(node, ForwardRefNode):
            if isinstance(node.state, RefUnresolved):
                # Resolution was never attempted
                ...
            elif isinstance(node.state, RefFailed):
                # Resolution failed: node.state.error has details
                ...
            else:
                # Cycle detected (RefResolved but returned self)
                ...
        ```

        Prefer the method form for chaining: ``node.resolved().children()``.
        Prefer the function form for map/filter:
        ``map(resolve_forward_ref, nodes)``.

    Example:
        Chained usage for immediate attribute access:

        >>> from typing_graph import ConcreteNode
        >>> node = ConcreteNode(cls=int)
        >>> node.resolved() is node
        True
        >>> node.resolved().children()
        ()
    """
    return self

typing_graph.MethodSig dataclass

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class MethodSig:
    """A method signature (not a TypeNode itself)."""

    name: str
    signature: SignatureNode | CallableNode
    is_classmethod: bool = False
    is_staticmethod: bool = False
    is_property: bool = False

Helper types

Supporting types for field definitions, parameters, and source locations.

Classes:

Name Description
FieldDef

A named field with a type (not a TypeNode itself).

DataclassFieldDef

Extended field definition for dataclasses.

Parameter

A callable parameter (not a TypeNode itself).

SourceLocation

Source code location for a type definition.

ModuleTypes

Collection of types discovered in a module.

Attributes:

Name Type Description
ClassInspectResult

Type alias for possible class inspection results.

typing_graph.ClassInspectResult module-attribute

typing_graph.FieldDef dataclass

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class FieldDef:
    """A named field with a type (not a TypeNode itself)."""

    name: str
    type: TypeNode
    required: bool = True
    metadata: MetadataCollection = field(
        default_factory=lambda: MetadataCollection.EMPTY
    )  # Metadata from Annotated on this field

typing_graph.DataclassFieldDef dataclass

Bases: FieldDef

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class DataclassFieldDef(FieldDef):
    """Extended field definition for dataclasses."""

    default: object | None = None
    default_factory: bool = False  # True if default is a factory
    init: bool = True
    repr: bool = True
    compare: bool = True
    kw_only: bool = False
    hash: bool | None = None

typing_graph.Parameter dataclass

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class Parameter:
    """A callable parameter (not a TypeNode itself)."""

    name: str
    type: TypeNode
    kind: str = "POSITIONAL_OR_KEYWORD"  # matches inspect.Parameter.Kind names
    default: object | None = None
    has_default: bool = False
    metadata: MetadataCollection = field(
        default_factory=lambda: MetadataCollection.EMPTY
    )

typing_graph.SourceLocation dataclass

Source code in src/typing_graph/_node.py
@dataclass(slots=True, frozen=True)
class SourceLocation:
    """Source code location for a type definition."""

    module: str | None = None
    qualname: str | None = None
    lineno: int | None = None
    file: str | None = None

typing_graph.ModuleTypes dataclass

Attributes:

Name Type Description
classes dict[str, ClassInspectResult]

Mapping of class names to their inspection results.

functions dict[str, FunctionNode]

Mapping of function names to their FunctionNode.

type_aliases dict[str, GenericAliasNode | TypeAliasNode]

Mapping of type alias names to their nodes.

type_vars dict[str, TypeVarNode | ParamSpecNode | TypeVarTupleNode]

Mapping of type variable names to their nodes.

constants dict[str, TypeNode]

Mapping of annotated constant names to their TypeNode.

Source code in src/typing_graph/_inspect_module.py
@dataclass(slots=True)
class ModuleTypes:
    """Collection of types discovered in a module.

    Attributes:
        classes: Mapping of class names to their inspection results.
        functions: Mapping of function names to their FunctionNode.
        type_aliases: Mapping of type alias names to their nodes.
        type_vars: Mapping of type variable names to their nodes.
        constants: Mapping of annotated constant names to their TypeNode.
    """

    classes: dict[str, ClassInspectResult] = field(default_factory=dict)
    functions: dict[str, "FunctionNode"] = field(default_factory=dict)
    type_aliases: dict[str, "GenericAliasNode | TypeAliasNode"] = field(
        default_factory=dict
    )
    type_vars: dict[str, "TypeVarNode | ParamSpecNode | TypeVarTupleNode"] = field(
        default_factory=dict
    )
    constants: dict[str, "TypeNode"] = field(default_factory=dict)

Metadata collection

Classes for working with type metadata.

Classes:

Name Description
MetadataCollection

Immutable collection of metadata from Annotated types.

SupportsLessThan

Protocol for types supporting the < operator.

typing_graph.MetadataCollection dataclass

Attributes:

Name Type Description
_items tuple[object, ...]

The internal tuple storing metadata items.

Examples:

>>> # Create an empty collection
>>> empty = MetadataCollection()
>>> len(empty)
0
>>> # Use the EMPTY singleton for empty collections
>>> MetadataCollection.EMPTY is MetadataCollection.EMPTY
True
>>> # Create a collection with items (factory method in later stage)
>>> coll = MetadataCollection(_items=("doc", 42, True))
>>> len(coll)
3
>>> list(coll)
['doc', 42, True]
>>> # Membership testing
>>> "doc" in coll
True
>>> "missing" in coll
False
>>> # Indexing and slicing
>>> coll[0]
'doc'
>>> coll[-1]
True

Methods:

Name Description
__len__

Return the number of items in the collection.

__iter__

Yield items in insertion order.

__contains__

Check if an item is in the collection using equality comparison.

__getitem__

Access items by index or slice.

__bool__

Return True if the collection is non-empty.

__eq__

Compare collections for equality.

__hash__

Return hash value if all items are hashable.

__repr__

Return a debug representation of the collection.

__add__

Concatenate two collections.

__or__

Concatenate two collections using the | operator.

find

Return the first item that is an instance of the given type.

find_first

Return the first item matching any of the given types.

has

Check if any item is an instance of any of the given types.

count

Count items that are instances of any of the given types.

find_all

Return all items that are instances of any of the given types.

get

Return the first matching item or a default value.

get_required

Return the first matching item or raise MetadataNotFoundError.

filter

Return items for which predicate returns True.

filter_by_type

Return items of given type for which predicate returns True.

first

Return first item for which predicate returns True.

first_of_type

Return first item of type matching optional predicate.

any

Return True if predicate returns True for any item.

find_protocol

Return items that satisfy the given protocol.

has_protocol

Return True if any item satisfies the given protocol.

count_protocol

Return count of items satisfying the given protocol.

of

Create a collection from an iterable.

from_annotated

Extract metadata from an Annotated type.

flatten

Return new collection with GroupedMetadata expanded (single level).

flatten_deep

Return new collection with GroupedMetadata recursively expanded.

exclude

Return items that are NOT instances of any of the given types.

unique

Return collection with duplicate items removed.

sorted

Return collection with items sorted.

reversed

Return collection with items in reverse order.

map

Apply a function to each item and return results as a tuple.

partition

Split collection into matching and non-matching items.

types

Return the set of unique types in the collection.

by_type

Group items by their type.

Source code in src/typing_graph/_metadata.py
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
@dataclass(slots=True, frozen=True)
class MetadataCollection:
    """Immutable collection of metadata from Annotated types.

    MetadataCollection provides a type-safe, immutable container for metadata
    extracted from Annotated type annotations. It implements the sequence
    protocol for familiar Python iteration patterns.

    The collection is frozen (immutable) and uses slots for memory efficiency.
    All transformation methods return new collections rather than modifying
    the existing one.

    Attributes:
        _items: The internal tuple storing metadata items.

    Examples:
        >>> # Create an empty collection
        >>> empty = MetadataCollection()
        >>> len(empty)
        0

        >>> # Use the EMPTY singleton for empty collections
        >>> MetadataCollection.EMPTY is MetadataCollection.EMPTY
        True

        >>> # Create a collection with items (factory method in later stage)
        >>> coll = MetadataCollection(_items=("doc", 42, True))
        >>> len(coll)
        3
        >>> list(coll)
        ['doc', 42, True]

        >>> # Membership testing
        >>> "doc" in coll
        True
        >>> "missing" in coll
        False

        >>> # Indexing and slicing
        >>> coll[0]
        'doc'
        >>> coll[-1]
        True
    """

    _items: tuple[object, ...] = field(default=())

    EMPTY: ClassVar[Self]
    """Singleton empty collection.

    Use this instead of creating new empty collections to avoid
    repeated allocations.
    """

    def __len__(self) -> int:
        """Return the number of items in the collection.

        Returns:
            The count of metadata items.

        Examples:
            >>> coll = MetadataCollection(_items=(1, 2, 3))
            >>> len(coll)
            3
        """
        return len(self._items)

    def __iter__(self) -> "Iterator[object]":
        """Yield items in insertion order.

        Yields:
            Each metadata item in the order it was added.

        Examples:
            >>> coll = MetadataCollection(_items=("a", "b", "c"))
            >>> list(coll)
            ['a', 'b', 'c']
        """
        return iter(self._items)

    def __contains__(self, item: object) -> bool:
        """Check if an item is in the collection using equality comparison.

        Args:
            item: The item to check for membership.

        Returns:
            True if the item is in the collection, False otherwise.

        Examples:
            >>> coll = MetadataCollection(_items=("doc", 42))
            >>> "doc" in coll
            True
            >>> "missing" in coll
            False
        """
        return item in self._items

    @overload
    def __getitem__(self, index: int) -> object: ...

    @overload
    def __getitem__(self, index: slice) -> "MetadataCollection": ...

    def __getitem__(self, index: int | slice) -> "object | MetadataCollection":
        """Access items by index or slice.

        Integer indexing returns the item at that position. Slice indexing
        returns a new MetadataCollection containing the sliced items.

        Args:
            index: An integer index or slice object.

        Returns:
            The item at the index (for int) or a new MetadataCollection
            containing the sliced items (for slice).

        Raises:
            IndexError: If the integer index is out of range.

        Examples:
            >>> coll = MetadataCollection(_items=("a", "b", "c", "d"))
            >>> coll[0]
            'a'
            >>> coll[-1]
            'd'
            >>> list(coll[1:3])
            ['b', 'c']
            >>> coll[::2]  # Returns MetadataCollection
            MetadataCollection(['a', 'c'])
        """
        if isinstance(index, int):
            return self._items[index]
        sliced = self._items[index]
        if not sliced:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=sliced)

    def __bool__(self) -> bool:
        """Return True if the collection is non-empty.

        Returns:
            True if the collection contains items, False if empty.

        Examples:
            >>> bool(MetadataCollection.EMPTY)
            False
            >>> bool(MetadataCollection(_items=(1,)))
            True
        """
        return bool(self._items)

    @override
    def __eq__(self, other: object) -> bool:
        """Compare collections for equality.

        Two MetadataCollections are equal if they contain the same items
        in the same order.

        Args:
            other: The object to compare with.

        Returns:
            True if other is a MetadataCollection with equal items,
            NotImplemented if other is not a MetadataCollection.

        Examples:
            >>> a = MetadataCollection(_items=(1, 2, 3))
            >>> b = MetadataCollection(_items=(1, 2, 3))
            >>> a == b
            True
            >>> c = MetadataCollection(_items=(3, 2, 1))
            >>> a == c
            False
        """
        if not isinstance(other, MetadataCollection):
            return NotImplemented
        # O(1) early-exit for different lengths
        if len(self._items) != len(other._items):
            return False
        return self._items == other._items

    @override
    def __hash__(self) -> int:
        """Return hash value if all items are hashable.

        The hash is computed from the tuple of items, enabling use of
        MetadataCollection as dict keys or set members when all items
        are hashable.

        Returns:
            Hash value based on the items tuple.

        Raises:
            TypeError: If any item in the collection is unhashable.
                The error message indicates which items caused the issue.

        Examples:
            >>> coll = MetadataCollection(_items=(1, "doc", (2, 3)))
            >>> isinstance(hash(coll), int)  # Works for hashable items
            True
            >>> coll_unhashable = MetadataCollection(_items=([1, 2],))
            >>> hash(coll_unhashable)  # doctest: +IGNORE_EXCEPTION_DETAIL
            Traceback (most recent call last):
                ...
            TypeError: MetadataCollection contains unhashable items...
        """
        try:
            return hash(self._items)
        except TypeError as e:
            msg = f"MetadataCollection contains unhashable items: {e}"
            raise TypeError(msg) from e

    @override
    def __repr__(self) -> str:
        """Return a debug representation of the collection.

        For collections with more than 5 items, the representation is
        truncated to show the first 5 items plus a count of remaining items.

        Returns:
            A string in the format MetadataCollection([item1, item2, ...]).

        Examples:
            >>> MetadataCollection(_items=())
            MetadataCollection([])
            >>> MetadataCollection(_items=(1, 2))
            MetadataCollection([1, 2])
            >>> MetadataCollection(_items=(1, 2, 3, 4, 5, 6, 7))
            MetadataCollection([1, 2, 3, 4, 5, ...<2 more>])
        """
        max_display = 5
        if len(self._items) <= max_display:
            items_repr = ", ".join(repr(item) for item in self._items)
            return f"MetadataCollection([{items_repr}])"
        displayed = ", ".join(repr(item) for item in self._items[:max_display])
        remaining = len(self._items) - max_display
        return f"MetadataCollection([{displayed}, ...<{remaining} more>])"

    def __add__(self, other: object) -> "MetadataCollection":
        """Concatenate two collections.

        Returns a new collection containing items from both collections,
        with self's items first followed by other's items.

        Args:
            other: Another MetadataCollection to concatenate.

        Returns:
            New MetadataCollection with concatenated items, or EMPTY if
            both collections are empty. Returns NotImplemented if other
            is not a MetadataCollection.

        Examples:
            >>> a = MetadataCollection(_items=(1, 2))
            >>> b = MetadataCollection(_items=(3, 4))
            >>> list(a + b)
            [1, 2, 3, 4]
            >>> empty = MetadataCollection.EMPTY
            >>> (empty + empty) is MetadataCollection.EMPTY
            True

        See Also:
            __or__: Equivalent operator using |.
        """
        if not isinstance(other, MetadataCollection):
            return NotImplemented
        combined = self._items + other._items
        if not combined:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=combined)

    def __or__(self, other: object) -> "MetadataCollection":
        """Concatenate two collections using the | operator.

        This is an alias for __add__, providing an alternative syntax
        for collection concatenation.

        Args:
            other: Another MetadataCollection to concatenate.

        Returns:
            New MetadataCollection with concatenated items.

        Examples:
            >>> a = MetadataCollection(_items=(1, 2))
            >>> b = MetadataCollection(_items=(3, 4))
            >>> list(a | b)
            [1, 2, 3, 4]

        See Also:
            __add__: Equivalent operator using +.
        """
        return self.__add__(other)

    @property
    def is_hashable(self) -> bool:
        """Check if all items are hashable without raising an exception.

        This property allows checking hashability before attempting
        operations that require it (like using the collection as a
        dict key or set member).

        Returns:
            True if all items are hashable, False otherwise.

        Examples:
            >>> coll = MetadataCollection(_items=(1, "doc"))
            >>> coll.is_hashable
            True
            >>> coll_unhashable = MetadataCollection(_items=([1, 2],))
            >>> coll_unhashable.is_hashable
            False

        See Also:
            __hash__: Compute hash value.
        """
        try:
            _ = hash(self._items)
        except TypeError:
            return False
        else:
            return True

    @property
    def is_empty(self) -> bool:
        """Check if the collection has no items.

        Returns:
            True if the collection contains no items, False otherwise.

        Examples:
            >>> MetadataCollection.EMPTY.is_empty
            True
            >>> MetadataCollection(_items=(1, 2)).is_empty
            False

        See Also:
            __bool__: Boolean conversion.
            __len__: Get item count.
        """
        return not self._items

    def find(self, type_: type[T]) -> T | None:
        """Return the first item that is an instance of the given type.

        Uses ``isinstance`` semantics, so subclasses match. For example,
        ``find(int)`` will match ``bool`` values since ``bool`` is a
        subclass of ``int``.

        Args:
            type_: The type to search for (including subclasses).

        Returns:
            The first matching item, or None if no match is found.

        Examples:
            >>> coll = MetadataCollection(_items=("doc", 42, True))
            >>> coll.find(int)  # Returns 42, not True (first match)
            42
            >>> coll.find(str)
            'doc'
            >>> coll.find(float) is None
            True

        See Also:
            find_first: Find first item matching any of several types.
            find_all: Find all items matching a type.
            get: Find with default value.
            get_required: Find or raise exception.
        """
        for item in self._items:
            if isinstance(item, type_):
                return item
        return None

    def find_first(self, *types: type) -> object | None:
        """Return the first item matching any of the given types.

        Args:
            *types: One or more types to search for.

        Returns:
            The first item that is an instance of any of the given types,
            or None if no match is found or no types are provided.

        Examples:
            >>> coll = MetadataCollection(_items=("doc", 42, True))
            >>> coll.find_first(int, bool)
            42
            >>> coll.find_first(float, complex) is None
            True
            >>> coll.find_first() is None
            True

        See Also:
            find: Find first item of exact type.
            find_all: Find all items matching types.
        """
        if not types:
            return None
        for item in self._items:
            if isinstance(item, types):
                return item
        return None

    def has(self, *types: type) -> bool:
        """Check if any item is an instance of any of the given types.

        Args:
            *types: One or more types to check for.

        Returns:
            True if any item matches any of the given types,
            False otherwise or if no types are provided.

        Examples:
            >>> coll = MetadataCollection(_items=("doc", 42))
            >>> coll.has(int)
            True
            >>> coll.has(float)
            False
            >>> coll.has(str, int)
            True
            >>> coll.has()
            False

        See Also:
            count: Count items matching types.
            any: Check with predicate instead of type.
        """
        if not types:
            return False
        return any(isinstance(item, types) for item in self._items)

    def count(self, *types: type) -> int:
        """Count items that are instances of any of the given types.

        Args:
            *types: One or more types to count.

        Returns:
            The number of items matching any of the given types,
            or 0 if no types are provided.

        Examples:
            >>> coll = MetadataCollection(_items=("a", "b", 1, 2, 3))
            >>> coll.count(str)
            2
            >>> coll.count(int)
            3
            >>> coll.count(str, int)
            5
            >>> coll.count()
            0

        See Also:
            has: Check existence without counting.
            count_protocol: Count items satisfying a protocol.
        """
        if not types:
            return 0
        return sum(1 for item in self._items if isinstance(item, types))

    @overload
    def find_all(self) -> "MetadataCollection": ...

    @overload
    def find_all(self, type_: type[T], /) -> "MetadataCollection": ...

    @overload
    def find_all(
        self, type_: type[T], type2_: type, /, *types: type
    ) -> "MetadataCollection": ...

    def find_all(self, *types: type) -> "MetadataCollection":
        """Return all items that are instances of any of the given types.

        Uses ``isinstance`` semantics, so subclasses match. For example,
        ``find_all(int)`` will match both ``int`` and ``bool`` values.

        If called with no arguments, returns a copy of the entire collection.

        Args:
            *types: Zero or more types to filter by (including subclasses).

        Returns:
            A new MetadataCollection containing matching items,
            or all items if no types are specified.

        Examples:
            >>> coll = MetadataCollection(_items=("a", 1, "b", 2))
            >>> list(coll.find_all())
            ['a', 1, 'b', 2]
            >>> list(coll.find_all(str))
            ['a', 'b']
            >>> list(coll.find_all(int, str))
            ['a', 1, 'b', 2]
            >>> coll.find_all(float) is MetadataCollection.EMPTY
            True

        See Also:
            find: Find first item of type.
            find_first: Find first item matching any type.
            filter_by_type: Filter with predicate.
        """
        if not types:
            # Return copy of all items
            if not self._items:
                return MetadataCollection.EMPTY
            return MetadataCollection(_items=self._items)
        matches = tuple(item for item in self._items if isinstance(item, types))
        if not matches:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=matches)

    @overload
    def get(self, type_: type[T]) -> T | None: ...

    @overload
    def get(self, type_: type[T], default: D) -> T | D: ...

    def get(self, type_: type[T], default: D | None = None) -> T | D | None:
        """Return the first matching item or a default value.

        Follows the ``dict.get()`` pattern for familiarity. Unlike ``find()``,
        this method correctly handles falsy values like ``0``, ``False``, or
        empty strings.

        Args:
            type_: The type to search for.
            default: Value to return if no match is found. Defaults to None.

        Returns:
            The first matching item, or the default value if not found.

        Examples:
            >>> coll = MetadataCollection(_items=("doc", 42))
            >>> coll.get(int)
            42
            >>> coll.get(float) is None
            True
            >>> coll.get(float, -1)
            -1
            >>> coll.get(float, "missing")
            'missing'
            >>> # Falsy values are returned correctly
            >>> coll = MetadataCollection(_items=(0, False, ""))
            >>> coll.get(int, -1)
            0
            >>> coll.get(bool, True)
            False

        See Also:
            find: Find without default value.
            get_required: Find or raise exception.
        """
        # Iterate directly instead of using find() to handle falsy values
        for item in self._items:
            if isinstance(item, type_):
                return item  # pyright narrows type after isinstance check
        return default

    def get_required(self, type_: type[T]) -> T:
        """Return the first matching item or raise MetadataNotFoundError.

        Use this method when the metadata is expected to exist. For optional
        metadata, use ``find()`` or ``get()`` instead.

        Args:
            type_: The type to search for.

        Returns:
            The first matching item.

        Raises:
            MetadataNotFoundError: If no item of the given type is found.

        Examples:
            >>> coll = MetadataCollection(_items=("doc", 42))
            >>> coll.get_required(int)
            42
            >>> coll.get_required(float)  # doctest: +IGNORE_EXCEPTION_DETAIL
            Traceback (most recent call last):
                ...
            MetadataNotFoundError: No metadata of type 'float' found...

        See Also:
            get: Find with default value.
            find: Find without raising.
        """
        # Iterate directly to correctly handle falsy values like 0, False, ""
        for item in self._items:
            if isinstance(item, type_):
                return item
        raise MetadataNotFoundError(type_, self)

    def filter(self, predicate: "Callable[[object], bool]") -> "MetadataCollection":
        """Return items for which predicate returns True.

        Args:
            predicate: Callable taking an item, returning True if it should be included.

        Returns:
            New MetadataCollection with matching items, or EMPTY if none match.

        Security:
            Predicates execute arbitrary code. Use only trusted sources.

        Examples:
            >>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
            >>> evens = coll.filter(lambda x: x % 2 == 0)
            >>> list(evens)
            [2, 4]

        See Also:
            filter_by_type: Filter with type safety.
            find_all: Filter by type only.
            exclude: Filter by excluding types.
        """
        matches = tuple(item for item in self._items if predicate(item))
        if not matches:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=matches)

    def filter_by_type(
        self, type_: type[T], predicate: "Callable[[T], bool]"
    ) -> "MetadataCollection":
        """Return items of given type for which predicate returns True.

        Provides type-safe filtering - predicate receives typed items.

        Args:
            type_: Type to filter by.
            predicate: Callable taking typed item, returning True to include.

        Returns:
            New MetadataCollection with matching items, or EMPTY if none match.

        Security:
            Predicates execute arbitrary code. Use only trusted sources.

        Examples:
            >>> coll = MetadataCollection(_items=("short", "medium", "verylongstring"))
            >>> long_strings = coll.filter_by_type(str, lambda s: len(s) > 6)
            >>> list(long_strings)
            ['verylongstring']

        See Also:
            filter: Filter without type restriction.
            find_all: Filter by type only.
        """
        matches = tuple(
            item for item in self._items if isinstance(item, type_) and predicate(item)
        )
        if not matches:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=matches)

    def first(self, predicate: "Callable[[object], bool]") -> object | None:
        """Return first item for which predicate returns True.

        Args:
            predicate: Callable taking an item, returning True if it matches.

        Returns:
            First matching item, or None if no match.

        Security:
            Predicates execute arbitrary code. Use only trusted sources.

        Examples:
            >>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
            >>> coll.first(lambda x: x > 3)
            4
            >>> coll.first(lambda x: x > 10) is None
            True

        See Also:
            first_of_type: Find first of type with predicate.
            find: Find by type without predicate.
            any: Check existence with predicate.
        """
        for item in self._items:
            if predicate(item):
                return item
        return None

    def first_of_type(
        self, type_: type[T], predicate: "Callable[[T], bool] | None" = None
    ) -> T | None:
        """Return first item of type matching optional predicate.

        Args:
            type_: Type to search for.
            predicate: Optional callable to filter typed items.

        Returns:
            First matching item, or None if no match.

        Security:
            Predicates execute arbitrary code. Use only trusted sources.

        Examples:
            >>> coll = MetadataCollection(_items=("a", 10, "bb", 20))
            >>> coll.first_of_type(int, lambda x: x > 15)
            20
            >>> coll.first_of_type(str)
            'a'

        See Also:
            first: Find with predicate only.
            find: Find by type only.
        """
        for item in self._items:
            if isinstance(item, type_) and (predicate is None or predicate(item)):
                return item
        return None

    def any(self, predicate: "Callable[[object], bool]") -> bool:
        """Return True if predicate returns True for any item.

        Args:
            predicate: Callable taking an item, returning bool.

        Returns:
            True if any item satisfies predicate, False otherwise.

        Security:
            Predicates execute arbitrary code. Use only trusted sources.

        Examples:
            >>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
            >>> coll.any(lambda x: x > 3)
            True
            >>> coll.any(lambda x: x > 10)
            False

        See Also:
            has: Check by type instead of predicate.
            first: Find the matching item.
            filter: Get all matching items.
        """
        return builtins.any(predicate(item) for item in self._items)

    def find_protocol(self, protocol: type) -> "MetadataCollection":
        """Return items that satisfy the given protocol.

        Args:
            protocol: A @runtime_checkable Protocol type.

        Returns:
            New MetadataCollection with matching items, or EMPTY if none match.

        Raises:
            TypeError: If protocol is not a Protocol.
            ProtocolNotRuntimeCheckableError: If protocol lacks @runtime_checkable.

        Security:
            Protocol types may have custom __subclasshook__. Use trusted sources.

        Examples:
            >>> from typing import Protocol, runtime_checkable
            >>> @runtime_checkable
            ... class HasValue(Protocol):
            ...     value: int
            >>> class Item:
            ...     value = 42
            >>> coll = MetadataCollection(_items=(Item(), "doc", 123))
            >>> matches = coll.find_protocol(HasValue)
            >>> len(matches)
            1

        See Also:
            has_protocol: Check protocol existence.
            count_protocol: Count protocol matches.
            filter: Filter with custom predicate.
        """
        _ensure_runtime_checkable(protocol)
        matches = tuple(item for item in self._items if isinstance(item, protocol))
        if not matches:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=matches)

    def has_protocol(self, protocol: type) -> bool:
        """Return True if any item satisfies the given protocol.

        Args:
            protocol: A @runtime_checkable Protocol type.

        Returns:
            True if any item satisfies the protocol.

        Raises:
            TypeError: If protocol is not a Protocol.
            ProtocolNotRuntimeCheckableError: If protocol lacks @runtime_checkable.

        Security:
            See find_protocol() for security considerations.

        Examples:
            >>> from typing import Protocol, runtime_checkable
            >>> @runtime_checkable
            ... class HasLen(Protocol):
            ...     def __len__(self) -> int: ...
            >>> coll = MetadataCollection(_items=([1, 2], "doc", 123))
            >>> coll.has_protocol(HasLen)
            True

        See Also:
            find_protocol: Get matching items.
            has: Check by type instead of protocol.
        """
        _ensure_runtime_checkable(protocol)
        return builtins.any(isinstance(item, protocol) for item in self._items)

    def count_protocol(self, protocol: type) -> int:
        """Return count of items satisfying the given protocol.

        Args:
            protocol: A @runtime_checkable Protocol type.

        Returns:
            Number of items satisfying the protocol.

        Raises:
            TypeError: If protocol is not a Protocol.
            ProtocolNotRuntimeCheckableError: If protocol lacks @runtime_checkable.

        Security:
            See find_protocol() for security considerations.

        Examples:
            >>> from typing import Protocol, runtime_checkable
            >>> @runtime_checkable
            ... class HasLen(Protocol):
            ...     def __len__(self) -> int: ...
            >>> coll = MetadataCollection(_items=([1, 2], "doc", 123, (3, 4)))
            >>> coll.count_protocol(HasLen)
            3

        See Also:
            find_protocol: Get matching items.
            count: Count by type instead of protocol.
        """
        _ensure_runtime_checkable(protocol)
        return sum(1 for item in self._items if isinstance(item, protocol))

    @classmethod
    def of(
        cls,
        items: "Iterable[object]" = (),
        *,
        auto_flatten: bool = True,
    ) -> "MetadataCollection":
        """Create a collection from an iterable.

        This is the primary factory method for creating MetadataCollection
        instances. It handles GroupedMetadata flattening automatically unless
        disabled.

        Args:
            items: Iterable of metadata objects.
            auto_flatten: If True (default), expand GroupedMetadata items
                one level. Set to False to preserve GroupedMetadata as-is.

        Returns:
            New MetadataCollection containing the items, or EMPTY if no items.

        Examples:
            >>> MetadataCollection.of([1, 2, 3])
            MetadataCollection([1, 2, 3])
            >>> MetadataCollection.of([])
            MetadataCollection([])
            >>> MetadataCollection.of([]) is MetadataCollection.EMPTY
            True

        See Also:
            from_annotated: Extract from Annotated types.
            EMPTY: Singleton empty collection.
        """
        if auto_flatten:
            flattened = _flatten_items(items)
            if not flattened:
                return cls.EMPTY
            return cls(_items=flattened)
        items_tuple = tuple(items)
        if not items_tuple:
            return cls.EMPTY
        return cls(_items=items_tuple)

    @classmethod
    def from_annotated(
        cls,
        annotated_type: object,
        *,
        unwrap_nested: bool = True,
    ) -> "MetadataCollection":
        """Extract metadata from an Annotated type.

        This method inspects a type and extracts any metadata from Annotated
        type hints. Non-Annotated types return an empty collection.

        Args:
            annotated_type: A type, potentially ``Annotated[T, ...]``.
            unwrap_nested: If True (default), recursively unwrap nested
                Annotated types, collecting all metadata. Outer metadata
                comes first, then inner metadata.

        Returns:
            MetadataCollection with extracted metadata, or EMPTY if the type
            is not Annotated or has no metadata.

        Examples:
            >>> from typing import Annotated
            >>> MetadataCollection.from_annotated(Annotated[int, "doc", 42])
            MetadataCollection(['doc', 42])
            >>> MetadataCollection.from_annotated(int)
            MetadataCollection([])
            >>> # Nested Annotated types are unwrapped by default
            >>> Inner = Annotated[int, "inner"]
            >>> Outer = Annotated[Inner, "outer"]
            >>> MetadataCollection.from_annotated(Outer)
            MetadataCollection(['inner', 'outer'])

        See Also:
            of: Create from any iterable.
        """
        # Check if it's an Annotated type
        origin = get_origin(annotated_type)
        if origin is not Annotated:
            return cls.EMPTY

        # Get the base type and metadata
        # get_args returns tuple[Any, ...] but we know it's safe for Annotated
        args: tuple[object, ...] = get_args(annotated_type)
        if len(args) < _MIN_ANNOTATED_ARGS:  # pragma: no cover
            # Defensive: valid Annotated always has >= 2 args
            return cls.EMPTY

        base_type: object = args[0]
        metadata: tuple[object, ...] = args[1:]

        # Collect all metadata items
        all_metadata: list[object] = list(metadata)

        # Recursively unwrap nested Annotated if requested
        # Note: Python auto-flattens nested Annotated at definition time,
        # so this branch handles edge cases from dynamic type construction
        if unwrap_nested:
            nested_origin: object = get_origin(base_type)
            if nested_origin is Annotated:  # pragma: no cover
                nested_collection = cls.from_annotated(base_type, unwrap_nested=True)
                all_metadata.extend(nested_collection)

        # Always flatten GroupedMetadata
        return cls.of(all_metadata, auto_flatten=True)

    def flatten(self) -> "MetadataCollection":
        """Return new collection with GroupedMetadata expanded (single level).

        This method expands any GroupedMetadata items one level, leaving
        nested GroupedMetadata intact. Use flatten_deep() for recursive
        expansion.

        Returns:
            New MetadataCollection with GroupedMetadata expanded one level,
            or self if no GroupedMetadata items exist.

        Examples:
            >>> coll = MetadataCollection(_items=(1, 2, 3))
            >>> coll.flatten()
            MetadataCollection([1, 2, 3])

        See Also:
            flatten_deep: Recursive flattening.
        """
        flattened = _flatten_items(self._items)
        if flattened == self._items:
            return self
        if not flattened:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=flattened)

    def flatten_deep(self) -> "MetadataCollection":
        """Return new collection with GroupedMetadata recursively expanded.

        This method repeatedly expands GroupedMetadata items until no more
        GroupedMetadata remains. Use flatten() for single-level expansion.

        Returns:
            New MetadataCollection with all GroupedMetadata fully expanded,
            or self if no GroupedMetadata items exist.

        Examples:
            >>> coll = MetadataCollection(_items=(1, 2, 3))
            >>> coll.flatten_deep()
            MetadataCollection([1, 2, 3])

        See Also:
            flatten: Single-level flattening.
        """
        current = self._items
        while True:
            # Check if any GroupedMetadata remains
            has_grouped = any(_is_grouped_metadata(item) for item in current)
            if not has_grouped:
                break
            current = _flatten_items(current)

        if current == self._items:
            return self
        if not current:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=current)

    def exclude(self, *types: type) -> "MetadataCollection":
        """Return items that are NOT instances of any of the given types.

        This is the inverse of find_all() - it excludes rather than includes
        items matching the specified types.

        Args:
            *types: One or more types to exclude.

        Returns:
            New MetadataCollection with non-matching items, or EMPTY if
            all items match. Returns self if no types are provided.

        Examples:
            >>> coll = MetadataCollection(_items=("a", 1, "b", 2))
            >>> list(coll.exclude(int))
            ['a', 'b']
            >>> list(coll.exclude(str, int))
            []
            >>> coll.exclude() is coll
            True

        See Also:
            filter: Filter with predicate.
            find_all: Keep items of types.
        """
        if not types:
            return self
        matches = tuple(item for item in self._items if not isinstance(item, types))
        if not matches:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=matches)

    def unique(self) -> "MetadataCollection":
        """Return collection with duplicate items removed.

        Preserves first occurrence order. Uses set-based deduplication
        for hashable items (O(n)), falling back to list-based comparison
        for unhashable items (O(n^2)).

        Returns:
            New MetadataCollection with unique items, or self if already unique.

        Examples:
            >>> coll = MetadataCollection(_items=(1, 2, 1, 3, 2))
            >>> list(coll.unique())
            [1, 2, 3]
            >>> # Unhashable items are handled
            >>> coll = MetadataCollection(_items=([1], [2], [1]))
            >>> list(coll.unique())
            [[1], [2]]

        See Also:
            sorted: Sort items.
        """
        if not self._items:
            return MetadataCollection.EMPTY

        # Try set-based deduplication (O(n))
        try:
            seen: set[object] = set()
            result: list[object] = []
            for item in self._items:
                if item not in seen:
                    seen.add(item)
                    result.append(item)
        except TypeError:
            # Fall back to list-based comparison for unhashable items (O(n^2))
            result = []
            for item in self._items:
                if item not in result:
                    result.append(item)

        result_tuple = tuple(result)
        if result_tuple == self._items:
            return self
        if not result_tuple:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=result_tuple)

    def sorted(
        self, *, key: "Callable[[object], SupportsLessThan] | None" = None
    ) -> "MetadataCollection":
        """Return collection with items sorted.

        Uses the provided key function for comparison. If no key is provided,
        uses a default key of (type_name, repr) for stable heterogeneous sorting.

        Args:
            key: Optional callable that extracts a comparison key from each item.
                The key must return a value supporting the < operator.

        Returns:
            New MetadataCollection with sorted items, or EMPTY if empty.

        Security:
            Key functions execute arbitrary code. Use only trusted sources.

        Examples:
            >>> coll = MetadataCollection(_items=(3, 1, 2))
            >>> list(coll.sorted())
            [1, 2, 3]
            >>> coll = MetadataCollection(_items=("b", "a", "c"))
            >>> list(coll.sorted())
            ['a', 'b', 'c']
            >>> # Custom key function
            >>> coll = MetadataCollection(_items=("bb", "a", "ccc"))
            >>> list(coll.sorted(key=len))
            ['a', 'bb', 'ccc']

        See Also:
            unique: Remove duplicates.
            reversed: Reverse order.
        """
        if not self._items:
            return MetadataCollection.EMPTY
        sort_key = key if key is not None else _default_sort_key
        sorted_items = tuple(builtins.sorted(self._items, key=sort_key))
        return MetadataCollection(_items=sorted_items)

    def reversed(self) -> "MetadataCollection":
        """Return collection with items in reverse order.

        Returns:
            New MetadataCollection with reversed items, or EMPTY if empty.

        Examples:
            >>> coll = MetadataCollection(_items=(1, 2, 3))
            >>> list(coll.reversed())
            [3, 2, 1]
            >>> MetadataCollection.EMPTY.reversed() is MetadataCollection.EMPTY
            True

        See Also:
            sorted: Sort items.
        """
        if not self._items:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=self._items[::-1])

    def map(self, func: "Callable[[object], T]") -> tuple[T, ...]:
        """Apply a function to each item and return results as a tuple.

        This is a terminal operation - it returns a tuple, not a
        MetadataCollection, because the transformed values may not be
        valid metadata items.

        Args:
            func: Callable to apply to each item.

        Returns:
            Tuple containing the results of applying func to each item.

        Security:
            Functions execute arbitrary code. Use only trusted sources.

        Examples:
            >>> coll = MetadataCollection(_items=(1, 2, 3))
            >>> coll.map(lambda x: x * 2)
            (2, 4, 6)
            >>> coll = MetadataCollection(_items=("a", "bb", "ccc"))
            >>> coll.map(len)
            (1, 2, 3)

        See Also:
            partition: Split collection by predicate.
            filter: Keep items matching predicate.
        """
        # List comprehension is faster than generator expression inside tuple()
        return tuple([func(item) for item in self._items])

    def partition(
        self, predicate: "Callable[[object], bool]"
    ) -> tuple["MetadataCollection", "MetadataCollection"]:
        """Split collection into matching and non-matching items.

        Args:
            predicate: Callable taking an item, returning True if it should
                be in the first partition.

        Returns:
            Tuple of (matching, non_matching) MetadataCollections.

        Security:
            Predicates execute arbitrary code. Use only trusted sources.

        Examples:
            >>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
            >>> matching, non_matching = coll.partition(lambda x: x % 2 == 0)
            >>> list(matching)
            [2, 4]
            >>> list(non_matching)
            [1, 3, 5]

        See Also:
            filter: Keep only matching items.
            map: Transform items.
        """
        matching: list[object] = []
        non_matching: list[object] = []
        for item in self._items:
            if predicate(item):
                matching.append(item)
            else:
                non_matching.append(item)

        matching_coll = (
            MetadataCollection(_items=tuple(matching))
            if matching
            else MetadataCollection.EMPTY
        )
        non_matching_coll = (
            MetadataCollection(_items=tuple(non_matching))
            if non_matching
            else MetadataCollection.EMPTY
        )
        return (matching_coll, non_matching_coll)

    def types(self) -> frozenset[type]:
        """Return the set of unique types in the collection.

        Returns:
            Frozenset containing the type of each unique item type.

        Examples:
            >>> coll = MetadataCollection(_items=("a", 1, "b", 2.0))
            >>> sorted(t.__name__ for t in coll.types())
            ['float', 'int', 'str']

        See Also:
            by_type: Group items by type.
        """
        return frozenset(type(item) for item in self._items)

    def by_type(self) -> "Mapping[type, tuple[object, ...]]":
        """Group items by their type.

        Returns:
            Immutable mapping from type to tuple of items of that type.
            Order within each group matches original insertion order.

        Examples:
            >>> coll = MetadataCollection(_items=("a", 1, "b", 2))
            >>> grouped = coll.by_type()
            >>> list(grouped[str])
            ['a', 'b']
            >>> list(grouped[int])
            [1, 2]

        See Also:
            types: Get unique types only.
        """
        groups: dict[type, list[object]] = {}
        for item in self._items:
            item_type = type(item)
            if item_type not in groups:
                groups[item_type] = []
            groups[item_type].append(item)
        # Convert lists to tuples for immutability
        result: dict[type, tuple[object, ...]] = {
            k: tuple(v) for k, v in groups.items()
        }
        return MappingProxyType(result)

EMPTY class-attribute

EMPTY: Self

is_hashable property

is_hashable: bool

Returns:

Type Description
bool

True if all items are hashable, False otherwise.

Examples:

>>> coll = MetadataCollection(_items=(1, "doc"))
>>> coll.is_hashable
True
>>> coll_unhashable = MetadataCollection(_items=([1, 2],))
>>> coll_unhashable.is_hashable
False

is_empty property

is_empty: bool

Returns:

Type Description
bool

True if the collection contains no items, False otherwise.

Examples:

>>> MetadataCollection.EMPTY.is_empty
True
>>> MetadataCollection(_items=(1, 2)).is_empty
False

__len__

__len__() -> int

Returns:

Type Description
int

The count of metadata items.

Examples:

>>> coll = MetadataCollection(_items=(1, 2, 3))
>>> len(coll)
3
Source code in src/typing_graph/_metadata.py
def __len__(self) -> int:
    """Return the number of items in the collection.

    Returns:
        The count of metadata items.

    Examples:
        >>> coll = MetadataCollection(_items=(1, 2, 3))
        >>> len(coll)
        3
    """
    return len(self._items)

__iter__

__iter__() -> Iterator[object]

Yields:

Type Description
object

Each metadata item in the order it was added.

Examples:

>>> coll = MetadataCollection(_items=("a", "b", "c"))
>>> list(coll)
['a', 'b', 'c']
Source code in src/typing_graph/_metadata.py
def __iter__(self) -> "Iterator[object]":
    """Yield items in insertion order.

    Yields:
        Each metadata item in the order it was added.

    Examples:
        >>> coll = MetadataCollection(_items=("a", "b", "c"))
        >>> list(coll)
        ['a', 'b', 'c']
    """
    return iter(self._items)

__contains__

__contains__(item: object) -> bool

Parameters:

Name Type Description Default
item object

The item to check for membership.

required

Returns:

Type Description
bool

True if the item is in the collection, False otherwise.

Examples:

>>> coll = MetadataCollection(_items=("doc", 42))
>>> "doc" in coll
True
>>> "missing" in coll
False
Source code in src/typing_graph/_metadata.py
def __contains__(self, item: object) -> bool:
    """Check if an item is in the collection using equality comparison.

    Args:
        item: The item to check for membership.

    Returns:
        True if the item is in the collection, False otherwise.

    Examples:
        >>> coll = MetadataCollection(_items=("doc", 42))
        >>> "doc" in coll
        True
        >>> "missing" in coll
        False
    """
    return item in self._items

__getitem__

__getitem__(index: int) -> object
__getitem__(index: slice) -> MetadataCollection
__getitem__(index: int | slice) -> object | MetadataCollection

Parameters:

Name Type Description Default
index int | slice

An integer index or slice object.

required

Returns:

Type Description
object | MetadataCollection

The item at the index (for int) or a new MetadataCollection

object | MetadataCollection

containing the sliced items (for slice).

Raises:

Type Description
IndexError

If the integer index is out of range.

Examples:

>>> coll = MetadataCollection(_items=("a", "b", "c", "d"))
>>> coll[0]
'a'
>>> coll[-1]
'd'
>>> list(coll[1:3])
['b', 'c']
>>> coll[::2]  # Returns MetadataCollection
MetadataCollection(['a', 'c'])
Source code in src/typing_graph/_metadata.py
def __getitem__(self, index: int | slice) -> "object | MetadataCollection":
    """Access items by index or slice.

    Integer indexing returns the item at that position. Slice indexing
    returns a new MetadataCollection containing the sliced items.

    Args:
        index: An integer index or slice object.

    Returns:
        The item at the index (for int) or a new MetadataCollection
        containing the sliced items (for slice).

    Raises:
        IndexError: If the integer index is out of range.

    Examples:
        >>> coll = MetadataCollection(_items=("a", "b", "c", "d"))
        >>> coll[0]
        'a'
        >>> coll[-1]
        'd'
        >>> list(coll[1:3])
        ['b', 'c']
        >>> coll[::2]  # Returns MetadataCollection
        MetadataCollection(['a', 'c'])
    """
    if isinstance(index, int):
        return self._items[index]
    sliced = self._items[index]
    if not sliced:
        return MetadataCollection.EMPTY
    return MetadataCollection(_items=sliced)

__bool__

__bool__() -> bool

Returns:

Type Description
bool

True if the collection contains items, False if empty.

Examples:

>>> bool(MetadataCollection.EMPTY)
False
>>> bool(MetadataCollection(_items=(1,)))
True
Source code in src/typing_graph/_metadata.py
def __bool__(self) -> bool:
    """Return True if the collection is non-empty.

    Returns:
        True if the collection contains items, False if empty.

    Examples:
        >>> bool(MetadataCollection.EMPTY)
        False
        >>> bool(MetadataCollection(_items=(1,)))
        True
    """
    return bool(self._items)

__eq__

__eq__(other: object) -> bool

Parameters:

Name Type Description Default
other object

The object to compare with.

required

Returns:

Type Description
bool

True if other is a MetadataCollection with equal items,

bool

NotImplemented if other is not a MetadataCollection.

Examples:

>>> a = MetadataCollection(_items=(1, 2, 3))
>>> b = MetadataCollection(_items=(1, 2, 3))
>>> a == b
True
>>> c = MetadataCollection(_items=(3, 2, 1))
>>> a == c
False
Source code in src/typing_graph/_metadata.py
@override
def __eq__(self, other: object) -> bool:
    """Compare collections for equality.

    Two MetadataCollections are equal if they contain the same items
    in the same order.

    Args:
        other: The object to compare with.

    Returns:
        True if other is a MetadataCollection with equal items,
        NotImplemented if other is not a MetadataCollection.

    Examples:
        >>> a = MetadataCollection(_items=(1, 2, 3))
        >>> b = MetadataCollection(_items=(1, 2, 3))
        >>> a == b
        True
        >>> c = MetadataCollection(_items=(3, 2, 1))
        >>> a == c
        False
    """
    if not isinstance(other, MetadataCollection):
        return NotImplemented
    # O(1) early-exit for different lengths
    if len(self._items) != len(other._items):
        return False
    return self._items == other._items

__hash__

__hash__() -> int

Returns:

Type Description
int

Hash value based on the items tuple.

Raises:

Type Description
TypeError

If any item in the collection is unhashable. The error message indicates which items caused the issue.

Examples:

>>> coll = MetadataCollection(_items=(1, "doc", (2, 3)))
>>> isinstance(hash(coll), int)  # Works for hashable items
True
>>> coll_unhashable = MetadataCollection(_items=([1, 2],))
>>> hash(coll_unhashable)
Traceback (most recent call last):
    ...
TypeError: MetadataCollection contains unhashable items...
Source code in src/typing_graph/_metadata.py
@override
def __hash__(self) -> int:
    """Return hash value if all items are hashable.

    The hash is computed from the tuple of items, enabling use of
    MetadataCollection as dict keys or set members when all items
    are hashable.

    Returns:
        Hash value based on the items tuple.

    Raises:
        TypeError: If any item in the collection is unhashable.
            The error message indicates which items caused the issue.

    Examples:
        >>> coll = MetadataCollection(_items=(1, "doc", (2, 3)))
        >>> isinstance(hash(coll), int)  # Works for hashable items
        True
        >>> coll_unhashable = MetadataCollection(_items=([1, 2],))
        >>> hash(coll_unhashable)  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
            ...
        TypeError: MetadataCollection contains unhashable items...
    """
    try:
        return hash(self._items)
    except TypeError as e:
        msg = f"MetadataCollection contains unhashable items: {e}"
        raise TypeError(msg) from e

__repr__

__repr__() -> str

Returns:

Type Description
str

A string in the format MetadataCollection([item1, item2, ...]).

Examples:

>>> MetadataCollection(_items=())
MetadataCollection([])
>>> MetadataCollection(_items=(1, 2))
MetadataCollection([1, 2])
>>> MetadataCollection(_items=(1, 2, 3, 4, 5, 6, 7))
MetadataCollection([1, 2, 3, 4, 5, ...<2 more>])
Source code in src/typing_graph/_metadata.py
@override
def __repr__(self) -> str:
    """Return a debug representation of the collection.

    For collections with more than 5 items, the representation is
    truncated to show the first 5 items plus a count of remaining items.

    Returns:
        A string in the format MetadataCollection([item1, item2, ...]).

    Examples:
        >>> MetadataCollection(_items=())
        MetadataCollection([])
        >>> MetadataCollection(_items=(1, 2))
        MetadataCollection([1, 2])
        >>> MetadataCollection(_items=(1, 2, 3, 4, 5, 6, 7))
        MetadataCollection([1, 2, 3, 4, 5, ...<2 more>])
    """
    max_display = 5
    if len(self._items) <= max_display:
        items_repr = ", ".join(repr(item) for item in self._items)
        return f"MetadataCollection([{items_repr}])"
    displayed = ", ".join(repr(item) for item in self._items[:max_display])
    remaining = len(self._items) - max_display
    return f"MetadataCollection([{displayed}, ...<{remaining} more>])"

__add__

__add__(other: object) -> MetadataCollection

Parameters:

Name Type Description Default
other object

Another MetadataCollection to concatenate.

required

Returns:

Type Description
MetadataCollection

New MetadataCollection with concatenated items, or EMPTY if

MetadataCollection

both collections are empty. Returns NotImplemented if other

MetadataCollection

is not a MetadataCollection.

Examples:

>>> a = MetadataCollection(_items=(1, 2))
>>> b = MetadataCollection(_items=(3, 4))
>>> list(a + b)
[1, 2, 3, 4]
>>> empty = MetadataCollection.EMPTY
>>> (empty + empty) is MetadataCollection.EMPTY
True
Source code in src/typing_graph/_metadata.py
def __add__(self, other: object) -> "MetadataCollection":
    """Concatenate two collections.

    Returns a new collection containing items from both collections,
    with self's items first followed by other's items.

    Args:
        other: Another MetadataCollection to concatenate.

    Returns:
        New MetadataCollection with concatenated items, or EMPTY if
        both collections are empty. Returns NotImplemented if other
        is not a MetadataCollection.

    Examples:
        >>> a = MetadataCollection(_items=(1, 2))
        >>> b = MetadataCollection(_items=(3, 4))
        >>> list(a + b)
        [1, 2, 3, 4]
        >>> empty = MetadataCollection.EMPTY
        >>> (empty + empty) is MetadataCollection.EMPTY
        True

    See Also:
        __or__: Equivalent operator using |.
    """
    if not isinstance(other, MetadataCollection):
        return NotImplemented
    combined = self._items + other._items
    if not combined:
        return MetadataCollection.EMPTY
    return MetadataCollection(_items=combined)

__or__

__or__(other: object) -> MetadataCollection

Parameters:

Name Type Description Default
other object

Another MetadataCollection to concatenate.

required

Returns:

Type Description
MetadataCollection

New MetadataCollection with concatenated items.

Examples:

>>> a = MetadataCollection(_items=(1, 2))
>>> b = MetadataCollection(_items=(3, 4))
>>> list(a | b)
[1, 2, 3, 4]
Source code in src/typing_graph/_metadata.py
def __or__(self, other: object) -> "MetadataCollection":
    """Concatenate two collections using the | operator.

    This is an alias for __add__, providing an alternative syntax
    for collection concatenation.

    Args:
        other: Another MetadataCollection to concatenate.

    Returns:
        New MetadataCollection with concatenated items.

    Examples:
        >>> a = MetadataCollection(_items=(1, 2))
        >>> b = MetadataCollection(_items=(3, 4))
        >>> list(a | b)
        [1, 2, 3, 4]

    See Also:
        __add__: Equivalent operator using +.
    """
    return self.__add__(other)

find

find(type_: type[T]) -> T | None

Parameters:

Name Type Description Default
type_ type[T]

The type to search for (including subclasses).

required

Returns:

Type Description
T | None

The first matching item, or None if no match is found.

Examples:

>>> coll = MetadataCollection(_items=("doc", 42, True))
>>> coll.find(int)  # Returns 42, not True (first match)
42
>>> coll.find(str)
'doc'
>>> coll.find(float) is None
True
Source code in src/typing_graph/_metadata.py
def find(self, type_: type[T]) -> T | None:
    """Return the first item that is an instance of the given type.

    Uses ``isinstance`` semantics, so subclasses match. For example,
    ``find(int)`` will match ``bool`` values since ``bool`` is a
    subclass of ``int``.

    Args:
        type_: The type to search for (including subclasses).

    Returns:
        The first matching item, or None if no match is found.

    Examples:
        >>> coll = MetadataCollection(_items=("doc", 42, True))
        >>> coll.find(int)  # Returns 42, not True (first match)
        42
        >>> coll.find(str)
        'doc'
        >>> coll.find(float) is None
        True

    See Also:
        find_first: Find first item matching any of several types.
        find_all: Find all items matching a type.
        get: Find with default value.
        get_required: Find or raise exception.
    """
    for item in self._items:
        if isinstance(item, type_):
            return item
    return None

find_first

find_first(*types: type) -> object | None

Parameters:

Name Type Description Default
*types type

One or more types to search for.

()

Returns:

Type Description
object | None

The first item that is an instance of any of the given types,

object | None

or None if no match is found or no types are provided.

Examples:

>>> coll = MetadataCollection(_items=("doc", 42, True))
>>> coll.find_first(int, bool)
42
>>> coll.find_first(float, complex) is None
True
>>> coll.find_first() is None
True
Source code in src/typing_graph/_metadata.py
def find_first(self, *types: type) -> object | None:
    """Return the first item matching any of the given types.

    Args:
        *types: One or more types to search for.

    Returns:
        The first item that is an instance of any of the given types,
        or None if no match is found or no types are provided.

    Examples:
        >>> coll = MetadataCollection(_items=("doc", 42, True))
        >>> coll.find_first(int, bool)
        42
        >>> coll.find_first(float, complex) is None
        True
        >>> coll.find_first() is None
        True

    See Also:
        find: Find first item of exact type.
        find_all: Find all items matching types.
    """
    if not types:
        return None
    for item in self._items:
        if isinstance(item, types):
            return item
    return None

has

has(*types: type) -> bool

Parameters:

Name Type Description Default
*types type

One or more types to check for.

()

Returns:

Type Description
bool

True if any item matches any of the given types,

bool

False otherwise or if no types are provided.

Examples:

>>> coll = MetadataCollection(_items=("doc", 42))
>>> coll.has(int)
True
>>> coll.has(float)
False
>>> coll.has(str, int)
True
>>> coll.has()
False
Source code in src/typing_graph/_metadata.py
def has(self, *types: type) -> bool:
    """Check if any item is an instance of any of the given types.

    Args:
        *types: One or more types to check for.

    Returns:
        True if any item matches any of the given types,
        False otherwise or if no types are provided.

    Examples:
        >>> coll = MetadataCollection(_items=("doc", 42))
        >>> coll.has(int)
        True
        >>> coll.has(float)
        False
        >>> coll.has(str, int)
        True
        >>> coll.has()
        False

    See Also:
        count: Count items matching types.
        any: Check with predicate instead of type.
    """
    if not types:
        return False
    return any(isinstance(item, types) for item in self._items)

count

count(*types: type) -> int

Parameters:

Name Type Description Default
*types type

One or more types to count.

()

Returns:

Type Description
int

The number of items matching any of the given types,

int

or 0 if no types are provided.

Examples:

>>> coll = MetadataCollection(_items=("a", "b", 1, 2, 3))
>>> coll.count(str)
2
>>> coll.count(int)
3
>>> coll.count(str, int)
5
>>> coll.count()
0
Source code in src/typing_graph/_metadata.py
def count(self, *types: type) -> int:
    """Count items that are instances of any of the given types.

    Args:
        *types: One or more types to count.

    Returns:
        The number of items matching any of the given types,
        or 0 if no types are provided.

    Examples:
        >>> coll = MetadataCollection(_items=("a", "b", 1, 2, 3))
        >>> coll.count(str)
        2
        >>> coll.count(int)
        3
        >>> coll.count(str, int)
        5
        >>> coll.count()
        0

    See Also:
        has: Check existence without counting.
        count_protocol: Count items satisfying a protocol.
    """
    if not types:
        return 0
    return sum(1 for item in self._items if isinstance(item, types))

find_all

find_all() -> MetadataCollection
find_all(type_: type[T]) -> MetadataCollection
find_all(type_: type[T], type2_: type, /, *types: type) -> MetadataCollection
find_all(*types: type) -> MetadataCollection

Parameters:

Name Type Description Default
*types type

Zero or more types to filter by (including subclasses).

()

Returns:

Type Description
MetadataCollection

A new MetadataCollection containing matching items,

MetadataCollection

or all items if no types are specified.

Examples:

>>> coll = MetadataCollection(_items=("a", 1, "b", 2))
>>> list(coll.find_all())
['a', 1, 'b', 2]
>>> list(coll.find_all(str))
['a', 'b']
>>> list(coll.find_all(int, str))
['a', 1, 'b', 2]
>>> coll.find_all(float) is MetadataCollection.EMPTY
True
Source code in src/typing_graph/_metadata.py
def find_all(self, *types: type) -> "MetadataCollection":
    """Return all items that are instances of any of the given types.

    Uses ``isinstance`` semantics, so subclasses match. For example,
    ``find_all(int)`` will match both ``int`` and ``bool`` values.

    If called with no arguments, returns a copy of the entire collection.

    Args:
        *types: Zero or more types to filter by (including subclasses).

    Returns:
        A new MetadataCollection containing matching items,
        or all items if no types are specified.

    Examples:
        >>> coll = MetadataCollection(_items=("a", 1, "b", 2))
        >>> list(coll.find_all())
        ['a', 1, 'b', 2]
        >>> list(coll.find_all(str))
        ['a', 'b']
        >>> list(coll.find_all(int, str))
        ['a', 1, 'b', 2]
        >>> coll.find_all(float) is MetadataCollection.EMPTY
        True

    See Also:
        find: Find first item of type.
        find_first: Find first item matching any type.
        filter_by_type: Filter with predicate.
    """
    if not types:
        # Return copy of all items
        if not self._items:
            return MetadataCollection.EMPTY
        return MetadataCollection(_items=self._items)
    matches = tuple(item for item in self._items if isinstance(item, types))
    if not matches:
        return MetadataCollection.EMPTY
    return MetadataCollection(_items=matches)

get

get(type_: type[T]) -> T | None
get(type_: type[T], default: D) -> T | D
get(type_: type[T], default: D | None = None) -> T | D | None

Parameters:

Name Type Description Default
type_ type[T]

The type to search for.

required
default D | None

Value to return if no match is found. Defaults to None.

None

Returns:

Type Description
T | D | None

The first matching item, or the default value if not found.

Examples:

>>> coll = MetadataCollection(_items=("doc", 42))
>>> coll.get(int)
42
>>> coll.get(float) is None
True
>>> coll.get(float, -1)
-1
>>> coll.get(float, "missing")
'missing'
>>> # Falsy values are returned correctly
>>> coll = MetadataCollection(_items=(0, False, ""))
>>> coll.get(int, -1)
0
>>> coll.get(bool, True)
False
Source code in src/typing_graph/_metadata.py
def get(self, type_: type[T], default: D | None = None) -> T | D | None:
    """Return the first matching item or a default value.

    Follows the ``dict.get()`` pattern for familiarity. Unlike ``find()``,
    this method correctly handles falsy values like ``0``, ``False``, or
    empty strings.

    Args:
        type_: The type to search for.
        default: Value to return if no match is found. Defaults to None.

    Returns:
        The first matching item, or the default value if not found.

    Examples:
        >>> coll = MetadataCollection(_items=("doc", 42))
        >>> coll.get(int)
        42
        >>> coll.get(float) is None
        True
        >>> coll.get(float, -1)
        -1
        >>> coll.get(float, "missing")
        'missing'
        >>> # Falsy values are returned correctly
        >>> coll = MetadataCollection(_items=(0, False, ""))
        >>> coll.get(int, -1)
        0
        >>> coll.get(bool, True)
        False

    See Also:
        find: Find without default value.
        get_required: Find or raise exception.
    """
    # Iterate directly instead of using find() to handle falsy values
    for item in self._items:
        if isinstance(item, type_):
            return item  # pyright narrows type after isinstance check
    return default

get_required

get_required(type_: type[T]) -> T

Parameters:

Name Type Description Default
type_ type[T]

The type to search for.

required

Returns:

Type Description
T

The first matching item.

Raises:

Type Description
MetadataNotFoundError

If no item of the given type is found.

Examples:

>>> coll = MetadataCollection(_items=("doc", 42))
>>> coll.get_required(int)
42
>>> coll.get_required(float)
Traceback (most recent call last):
    ...
MetadataNotFoundError: No metadata of type 'float' found...
Source code in src/typing_graph/_metadata.py
def get_required(self, type_: type[T]) -> T:
    """Return the first matching item or raise MetadataNotFoundError.

    Use this method when the metadata is expected to exist. For optional
    metadata, use ``find()`` or ``get()`` instead.

    Args:
        type_: The type to search for.

    Returns:
        The first matching item.

    Raises:
        MetadataNotFoundError: If no item of the given type is found.

    Examples:
        >>> coll = MetadataCollection(_items=("doc", 42))
        >>> coll.get_required(int)
        42
        >>> coll.get_required(float)  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
            ...
        MetadataNotFoundError: No metadata of type 'float' found...

    See Also:
        get: Find with default value.
        find: Find without raising.
    """
    # Iterate directly to correctly handle falsy values like 0, False, ""
    for item in self._items:
        if isinstance(item, type_):
            return item
    raise MetadataNotFoundError(type_, self)

filter

filter(predicate: Callable[[object], bool]) -> MetadataCollection

Parameters:

Name Type Description Default
predicate Callable[[object], bool]

Callable taking an item, returning True if it should be included.

required

Returns:

Type Description
MetadataCollection

New MetadataCollection with matching items, or EMPTY if none match.

Examples:

>>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
>>> evens = coll.filter(lambda x: x % 2 == 0)
>>> list(evens)
[2, 4]
Source code in src/typing_graph/_metadata.py
def filter(self, predicate: "Callable[[object], bool]") -> "MetadataCollection":
    """Return items for which predicate returns True.

    Args:
        predicate: Callable taking an item, returning True if it should be included.

    Returns:
        New MetadataCollection with matching items, or EMPTY if none match.

    Security:
        Predicates execute arbitrary code. Use only trusted sources.

    Examples:
        >>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
        >>> evens = coll.filter(lambda x: x % 2 == 0)
        >>> list(evens)
        [2, 4]

    See Also:
        filter_by_type: Filter with type safety.
        find_all: Filter by type only.
        exclude: Filter by excluding types.
    """
    matches = tuple(item for item in self._items if predicate(item))
    if not matches:
        return MetadataCollection.EMPTY
    return MetadataCollection(_items=matches)

filter_by_type

filter_by_type(type_: type[T], predicate: Callable[[T], bool]) -> MetadataCollection

Parameters:

Name Type Description Default
type_ type[T]

Type to filter by.

required
predicate Callable[[T], bool]

Callable taking typed item, returning True to include.

required

Returns:

Type Description
MetadataCollection

New MetadataCollection with matching items, or EMPTY if none match.

Examples:

>>> coll = MetadataCollection(_items=("short", "medium", "verylongstring"))
>>> long_strings = coll.filter_by_type(str, lambda s: len(s) > 6)
>>> list(long_strings)
['verylongstring']
Source code in src/typing_graph/_metadata.py
def filter_by_type(
    self, type_: type[T], predicate: "Callable[[T], bool]"
) -> "MetadataCollection":
    """Return items of given type for which predicate returns True.

    Provides type-safe filtering - predicate receives typed items.

    Args:
        type_: Type to filter by.
        predicate: Callable taking typed item, returning True to include.

    Returns:
        New MetadataCollection with matching items, or EMPTY if none match.

    Security:
        Predicates execute arbitrary code. Use only trusted sources.

    Examples:
        >>> coll = MetadataCollection(_items=("short", "medium", "verylongstring"))
        >>> long_strings = coll.filter_by_type(str, lambda s: len(s) > 6)
        >>> list(long_strings)
        ['verylongstring']

    See Also:
        filter: Filter without type restriction.
        find_all: Filter by type only.
    """
    matches = tuple(
        item for item in self._items if isinstance(item, type_) and predicate(item)
    )
    if not matches:
        return MetadataCollection.EMPTY
    return MetadataCollection(_items=matches)

first

first(predicate: Callable[[object], bool]) -> object | None

Parameters:

Name Type Description Default
predicate Callable[[object], bool]

Callable taking an item, returning True if it matches.

required

Returns:

Type Description
object | None

First matching item, or None if no match.

Examples:

>>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
>>> coll.first(lambda x: x > 3)
4
>>> coll.first(lambda x: x > 10) is None
True
Source code in src/typing_graph/_metadata.py
def first(self, predicate: "Callable[[object], bool]") -> object | None:
    """Return first item for which predicate returns True.

    Args:
        predicate: Callable taking an item, returning True if it matches.

    Returns:
        First matching item, or None if no match.

    Security:
        Predicates execute arbitrary code. Use only trusted sources.

    Examples:
        >>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
        >>> coll.first(lambda x: x > 3)
        4
        >>> coll.first(lambda x: x > 10) is None
        True

    See Also:
        first_of_type: Find first of type with predicate.
        find: Find by type without predicate.
        any: Check existence with predicate.
    """
    for item in self._items:
        if predicate(item):
            return item
    return None

first_of_type

first_of_type(
    type_: type[T], predicate: Callable[[T], bool] | None = None
) -> T | None

Parameters:

Name Type Description Default
type_ type[T]

Type to search for.

required
predicate Callable[[T], bool] | None

Optional callable to filter typed items.

None

Returns:

Type Description
T | None

First matching item, or None if no match.

Examples:

>>> coll = MetadataCollection(_items=("a", 10, "bb", 20))
>>> coll.first_of_type(int, lambda x: x > 15)
20
>>> coll.first_of_type(str)
'a'
Source code in src/typing_graph/_metadata.py
def first_of_type(
    self, type_: type[T], predicate: "Callable[[T], bool] | None" = None
) -> T | None:
    """Return first item of type matching optional predicate.

    Args:
        type_: Type to search for.
        predicate: Optional callable to filter typed items.

    Returns:
        First matching item, or None if no match.

    Security:
        Predicates execute arbitrary code. Use only trusted sources.

    Examples:
        >>> coll = MetadataCollection(_items=("a", 10, "bb", 20))
        >>> coll.first_of_type(int, lambda x: x > 15)
        20
        >>> coll.first_of_type(str)
        'a'

    See Also:
        first: Find with predicate only.
        find: Find by type only.
    """
    for item in self._items:
        if isinstance(item, type_) and (predicate is None or predicate(item)):
            return item
    return None

any

any(predicate: Callable[[object], bool]) -> bool

Parameters:

Name Type Description Default
predicate Callable[[object], bool]

Callable taking an item, returning bool.

required

Returns:

Type Description
bool

True if any item satisfies predicate, False otherwise.

Examples:

>>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
>>> coll.any(lambda x: x > 3)
True
>>> coll.any(lambda x: x > 10)
False
Source code in src/typing_graph/_metadata.py
def any(self, predicate: "Callable[[object], bool]") -> bool:
    """Return True if predicate returns True for any item.

    Args:
        predicate: Callable taking an item, returning bool.

    Returns:
        True if any item satisfies predicate, False otherwise.

    Security:
        Predicates execute arbitrary code. Use only trusted sources.

    Examples:
        >>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
        >>> coll.any(lambda x: x > 3)
        True
        >>> coll.any(lambda x: x > 10)
        False

    See Also:
        has: Check by type instead of predicate.
        first: Find the matching item.
        filter: Get all matching items.
    """
    return builtins.any(predicate(item) for item in self._items)

find_protocol

find_protocol(protocol: type) -> MetadataCollection

Parameters:

Name Type Description Default
protocol type

A @runtime_checkable Protocol type.

required

Returns:

Type Description
MetadataCollection

New MetadataCollection with matching items, or EMPTY if none match.

Raises:

Type Description
TypeError

If protocol is not a Protocol.

ProtocolNotRuntimeCheckableError

If protocol lacks @runtime_checkable.

Examples:

>>> from typing import Protocol, runtime_checkable
>>> @runtime_checkable
... class HasValue(Protocol):
...     value: int
>>> class Item:
...     value = 42
>>> coll = MetadataCollection(_items=(Item(), "doc", 123))
>>> matches = coll.find_protocol(HasValue)
>>> len(matches)
1
Source code in src/typing_graph/_metadata.py
def find_protocol(self, protocol: type) -> "MetadataCollection":
    """Return items that satisfy the given protocol.

    Args:
        protocol: A @runtime_checkable Protocol type.

    Returns:
        New MetadataCollection with matching items, or EMPTY if none match.

    Raises:
        TypeError: If protocol is not a Protocol.
        ProtocolNotRuntimeCheckableError: If protocol lacks @runtime_checkable.

    Security:
        Protocol types may have custom __subclasshook__. Use trusted sources.

    Examples:
        >>> from typing import Protocol, runtime_checkable
        >>> @runtime_checkable
        ... class HasValue(Protocol):
        ...     value: int
        >>> class Item:
        ...     value = 42
        >>> coll = MetadataCollection(_items=(Item(), "doc", 123))
        >>> matches = coll.find_protocol(HasValue)
        >>> len(matches)
        1

    See Also:
        has_protocol: Check protocol existence.
        count_protocol: Count protocol matches.
        filter: Filter with custom predicate.
    """
    _ensure_runtime_checkable(protocol)
    matches = tuple(item for item in self._items if isinstance(item, protocol))
    if not matches:
        return MetadataCollection.EMPTY
    return MetadataCollection(_items=matches)

has_protocol

has_protocol(protocol: type) -> bool

Parameters:

Name Type Description Default
protocol type

A @runtime_checkable Protocol type.

required

Returns:

Type Description
bool

True if any item satisfies the protocol.

Raises:

Type Description
TypeError

If protocol is not a Protocol.

ProtocolNotRuntimeCheckableError

If protocol lacks @runtime_checkable.

Examples:

>>> from typing import Protocol, runtime_checkable
>>> @runtime_checkable
... class HasLen(Protocol):
...     def __len__(self) -> int: ...
>>> coll = MetadataCollection(_items=([1, 2], "doc", 123))
>>> coll.has_protocol(HasLen)
True
Source code in src/typing_graph/_metadata.py
def has_protocol(self, protocol: type) -> bool:
    """Return True if any item satisfies the given protocol.

    Args:
        protocol: A @runtime_checkable Protocol type.

    Returns:
        True if any item satisfies the protocol.

    Raises:
        TypeError: If protocol is not a Protocol.
        ProtocolNotRuntimeCheckableError: If protocol lacks @runtime_checkable.

    Security:
        See find_protocol() for security considerations.

    Examples:
        >>> from typing import Protocol, runtime_checkable
        >>> @runtime_checkable
        ... class HasLen(Protocol):
        ...     def __len__(self) -> int: ...
        >>> coll = MetadataCollection(_items=([1, 2], "doc", 123))
        >>> coll.has_protocol(HasLen)
        True

    See Also:
        find_protocol: Get matching items.
        has: Check by type instead of protocol.
    """
    _ensure_runtime_checkable(protocol)
    return builtins.any(isinstance(item, protocol) for item in self._items)

count_protocol

count_protocol(protocol: type) -> int

Parameters:

Name Type Description Default
protocol type

A @runtime_checkable Protocol type.

required

Returns:

Type Description
int

Number of items satisfying the protocol.

Raises:

Type Description
TypeError

If protocol is not a Protocol.

ProtocolNotRuntimeCheckableError

If protocol lacks @runtime_checkable.

Examples:

>>> from typing import Protocol, runtime_checkable
>>> @runtime_checkable
... class HasLen(Protocol):
...     def __len__(self) -> int: ...
>>> coll = MetadataCollection(_items=([1, 2], "doc", 123, (3, 4)))
>>> coll.count_protocol(HasLen)
3
Source code in src/typing_graph/_metadata.py
def count_protocol(self, protocol: type) -> int:
    """Return count of items satisfying the given protocol.

    Args:
        protocol: A @runtime_checkable Protocol type.

    Returns:
        Number of items satisfying the protocol.

    Raises:
        TypeError: If protocol is not a Protocol.
        ProtocolNotRuntimeCheckableError: If protocol lacks @runtime_checkable.

    Security:
        See find_protocol() for security considerations.

    Examples:
        >>> from typing import Protocol, runtime_checkable
        >>> @runtime_checkable
        ... class HasLen(Protocol):
        ...     def __len__(self) -> int: ...
        >>> coll = MetadataCollection(_items=([1, 2], "doc", 123, (3, 4)))
        >>> coll.count_protocol(HasLen)
        3

    See Also:
        find_protocol: Get matching items.
        count: Count by type instead of protocol.
    """
    _ensure_runtime_checkable(protocol)
    return sum(1 for item in self._items if isinstance(item, protocol))

of classmethod

of(items: Iterable[object] = (), *, auto_flatten: bool = True) -> MetadataCollection

Parameters:

Name Type Description Default
items Iterable[object]

Iterable of metadata objects.

()
auto_flatten bool

If True (default), expand GroupedMetadata items one level. Set to False to preserve GroupedMetadata as-is.

True

Returns:

Type Description
MetadataCollection

New MetadataCollection containing the items, or EMPTY if no items.

Examples:

>>> MetadataCollection.of([1, 2, 3])
MetadataCollection([1, 2, 3])
>>> MetadataCollection.of([])
MetadataCollection([])
>>> MetadataCollection.of([]) is MetadataCollection.EMPTY
True
Source code in src/typing_graph/_metadata.py
@classmethod
def of(
    cls,
    items: "Iterable[object]" = (),
    *,
    auto_flatten: bool = True,
) -> "MetadataCollection":
    """Create a collection from an iterable.

    This is the primary factory method for creating MetadataCollection
    instances. It handles GroupedMetadata flattening automatically unless
    disabled.

    Args:
        items: Iterable of metadata objects.
        auto_flatten: If True (default), expand GroupedMetadata items
            one level. Set to False to preserve GroupedMetadata as-is.

    Returns:
        New MetadataCollection containing the items, or EMPTY if no items.

    Examples:
        >>> MetadataCollection.of([1, 2, 3])
        MetadataCollection([1, 2, 3])
        >>> MetadataCollection.of([])
        MetadataCollection([])
        >>> MetadataCollection.of([]) is MetadataCollection.EMPTY
        True

    See Also:
        from_annotated: Extract from Annotated types.
        EMPTY: Singleton empty collection.
    """
    if auto_flatten:
        flattened = _flatten_items(items)
        if not flattened:
            return cls.EMPTY
        return cls(_items=flattened)
    items_tuple = tuple(items)
    if not items_tuple:
        return cls.EMPTY
    return cls(_items=items_tuple)

from_annotated classmethod

from_annotated(
    annotated_type: object, *, unwrap_nested: bool = True
) -> MetadataCollection

Parameters:

Name Type Description Default
annotated_type object

A type, potentially Annotated[T, ...].

required
unwrap_nested bool

If True (default), recursively unwrap nested Annotated types, collecting all metadata. Outer metadata comes first, then inner metadata.

True

Returns:

Type Description
MetadataCollection

MetadataCollection with extracted metadata, or EMPTY if the type

MetadataCollection

is not Annotated or has no metadata.

Examples:

>>> from typing import Annotated
>>> MetadataCollection.from_annotated(Annotated[int, "doc", 42])
MetadataCollection(['doc', 42])
>>> MetadataCollection.from_annotated(int)
MetadataCollection([])
>>> # Nested Annotated types are unwrapped by default
>>> Inner = Annotated[int, "inner"]
>>> Outer = Annotated[Inner, "outer"]
>>> MetadataCollection.from_annotated(Outer)
MetadataCollection(['inner', 'outer'])
Source code in src/typing_graph/_metadata.py
@classmethod
def from_annotated(
    cls,
    annotated_type: object,
    *,
    unwrap_nested: bool = True,
) -> "MetadataCollection":
    """Extract metadata from an Annotated type.

    This method inspects a type and extracts any metadata from Annotated
    type hints. Non-Annotated types return an empty collection.

    Args:
        annotated_type: A type, potentially ``Annotated[T, ...]``.
        unwrap_nested: If True (default), recursively unwrap nested
            Annotated types, collecting all metadata. Outer metadata
            comes first, then inner metadata.

    Returns:
        MetadataCollection with extracted metadata, or EMPTY if the type
        is not Annotated or has no metadata.

    Examples:
        >>> from typing import Annotated
        >>> MetadataCollection.from_annotated(Annotated[int, "doc", 42])
        MetadataCollection(['doc', 42])
        >>> MetadataCollection.from_annotated(int)
        MetadataCollection([])
        >>> # Nested Annotated types are unwrapped by default
        >>> Inner = Annotated[int, "inner"]
        >>> Outer = Annotated[Inner, "outer"]
        >>> MetadataCollection.from_annotated(Outer)
        MetadataCollection(['inner', 'outer'])

    See Also:
        of: Create from any iterable.
    """
    # Check if it's an Annotated type
    origin = get_origin(annotated_type)
    if origin is not Annotated:
        return cls.EMPTY

    # Get the base type and metadata
    # get_args returns tuple[Any, ...] but we know it's safe for Annotated
    args: tuple[object, ...] = get_args(annotated_type)
    if len(args) < _MIN_ANNOTATED_ARGS:  # pragma: no cover
        # Defensive: valid Annotated always has >= 2 args
        return cls.EMPTY

    base_type: object = args[0]
    metadata: tuple[object, ...] = args[1:]

    # Collect all metadata items
    all_metadata: list[object] = list(metadata)

    # Recursively unwrap nested Annotated if requested
    # Note: Python auto-flattens nested Annotated at definition time,
    # so this branch handles edge cases from dynamic type construction
    if unwrap_nested:
        nested_origin: object = get_origin(base_type)
        if nested_origin is Annotated:  # pragma: no cover
            nested_collection = cls.from_annotated(base_type, unwrap_nested=True)
            all_metadata.extend(nested_collection)

    # Always flatten GroupedMetadata
    return cls.of(all_metadata, auto_flatten=True)

flatten

flatten() -> MetadataCollection

Returns:

Type Description
MetadataCollection

New MetadataCollection with GroupedMetadata expanded one level,

MetadataCollection

or self if no GroupedMetadata items exist.

Examples:

>>> coll = MetadataCollection(_items=(1, 2, 3))
>>> coll.flatten()
MetadataCollection([1, 2, 3])
Source code in src/typing_graph/_metadata.py
def flatten(self) -> "MetadataCollection":
    """Return new collection with GroupedMetadata expanded (single level).

    This method expands any GroupedMetadata items one level, leaving
    nested GroupedMetadata intact. Use flatten_deep() for recursive
    expansion.

    Returns:
        New MetadataCollection with GroupedMetadata expanded one level,
        or self if no GroupedMetadata items exist.

    Examples:
        >>> coll = MetadataCollection(_items=(1, 2, 3))
        >>> coll.flatten()
        MetadataCollection([1, 2, 3])

    See Also:
        flatten_deep: Recursive flattening.
    """
    flattened = _flatten_items(self._items)
    if flattened == self._items:
        return self
    if not flattened:
        return MetadataCollection.EMPTY
    return MetadataCollection(_items=flattened)

flatten_deep

flatten_deep() -> MetadataCollection

Returns:

Type Description
MetadataCollection

New MetadataCollection with all GroupedMetadata fully expanded,

MetadataCollection

or self if no GroupedMetadata items exist.

Examples:

>>> coll = MetadataCollection(_items=(1, 2, 3))
>>> coll.flatten_deep()
MetadataCollection([1, 2, 3])
Source code in src/typing_graph/_metadata.py
def flatten_deep(self) -> "MetadataCollection":
    """Return new collection with GroupedMetadata recursively expanded.

    This method repeatedly expands GroupedMetadata items until no more
    GroupedMetadata remains. Use flatten() for single-level expansion.

    Returns:
        New MetadataCollection with all GroupedMetadata fully expanded,
        or self if no GroupedMetadata items exist.

    Examples:
        >>> coll = MetadataCollection(_items=(1, 2, 3))
        >>> coll.flatten_deep()
        MetadataCollection([1, 2, 3])

    See Also:
        flatten: Single-level flattening.
    """
    current = self._items
    while True:
        # Check if any GroupedMetadata remains
        has_grouped = any(_is_grouped_metadata(item) for item in current)
        if not has_grouped:
            break
        current = _flatten_items(current)

    if current == self._items:
        return self
    if not current:
        return MetadataCollection.EMPTY
    return MetadataCollection(_items=current)

exclude

exclude(*types: type) -> MetadataCollection

Parameters:

Name Type Description Default
*types type

One or more types to exclude.

()

Returns:

Type Description
MetadataCollection

New MetadataCollection with non-matching items, or EMPTY if

MetadataCollection

all items match. Returns self if no types are provided.

Examples:

>>> coll = MetadataCollection(_items=("a", 1, "b", 2))
>>> list(coll.exclude(int))
['a', 'b']
>>> list(coll.exclude(str, int))
[]
>>> coll.exclude() is coll
True
Source code in src/typing_graph/_metadata.py
def exclude(self, *types: type) -> "MetadataCollection":
    """Return items that are NOT instances of any of the given types.

    This is the inverse of find_all() - it excludes rather than includes
    items matching the specified types.

    Args:
        *types: One or more types to exclude.

    Returns:
        New MetadataCollection with non-matching items, or EMPTY if
        all items match. Returns self if no types are provided.

    Examples:
        >>> coll = MetadataCollection(_items=("a", 1, "b", 2))
        >>> list(coll.exclude(int))
        ['a', 'b']
        >>> list(coll.exclude(str, int))
        []
        >>> coll.exclude() is coll
        True

    See Also:
        filter: Filter with predicate.
        find_all: Keep items of types.
    """
    if not types:
        return self
    matches = tuple(item for item in self._items if not isinstance(item, types))
    if not matches:
        return MetadataCollection.EMPTY
    return MetadataCollection(_items=matches)

unique

unique() -> MetadataCollection

Returns:

Type Description
MetadataCollection

New MetadataCollection with unique items, or self if already unique.

Examples:

>>> coll = MetadataCollection(_items=(1, 2, 1, 3, 2))
>>> list(coll.unique())
[1, 2, 3]
>>> # Unhashable items are handled
>>> coll = MetadataCollection(_items=([1], [2], [1]))
>>> list(coll.unique())
[[1], [2]]
Source code in src/typing_graph/_metadata.py
def unique(self) -> "MetadataCollection":
    """Return collection with duplicate items removed.

    Preserves first occurrence order. Uses set-based deduplication
    for hashable items (O(n)), falling back to list-based comparison
    for unhashable items (O(n^2)).

    Returns:
        New MetadataCollection with unique items, or self if already unique.

    Examples:
        >>> coll = MetadataCollection(_items=(1, 2, 1, 3, 2))
        >>> list(coll.unique())
        [1, 2, 3]
        >>> # Unhashable items are handled
        >>> coll = MetadataCollection(_items=([1], [2], [1]))
        >>> list(coll.unique())
        [[1], [2]]

    See Also:
        sorted: Sort items.
    """
    if not self._items:
        return MetadataCollection.EMPTY

    # Try set-based deduplication (O(n))
    try:
        seen: set[object] = set()
        result: list[object] = []
        for item in self._items:
            if item not in seen:
                seen.add(item)
                result.append(item)
    except TypeError:
        # Fall back to list-based comparison for unhashable items (O(n^2))
        result = []
        for item in self._items:
            if item not in result:
                result.append(item)

    result_tuple = tuple(result)
    if result_tuple == self._items:
        return self
    if not result_tuple:
        return MetadataCollection.EMPTY
    return MetadataCollection(_items=result_tuple)

sorted

sorted(
    *, key: Callable[[object], SupportsLessThan] | None = None
) -> MetadataCollection

Parameters:

Name Type Description Default
key Callable[[object], SupportsLessThan] | None

Optional callable that extracts a comparison key from each item. The key must return a value supporting the < operator.

None

Returns:

Type Description
MetadataCollection

New MetadataCollection with sorted items, or EMPTY if empty.

Examples:

>>> coll = MetadataCollection(_items=(3, 1, 2))
>>> list(coll.sorted())
[1, 2, 3]
>>> coll = MetadataCollection(_items=("b", "a", "c"))
>>> list(coll.sorted())
['a', 'b', 'c']
>>> # Custom key function
>>> coll = MetadataCollection(_items=("bb", "a", "ccc"))
>>> list(coll.sorted(key=len))
['a', 'bb', 'ccc']
Source code in src/typing_graph/_metadata.py
def sorted(
    self, *, key: "Callable[[object], SupportsLessThan] | None" = None
) -> "MetadataCollection":
    """Return collection with items sorted.

    Uses the provided key function for comparison. If no key is provided,
    uses a default key of (type_name, repr) for stable heterogeneous sorting.

    Args:
        key: Optional callable that extracts a comparison key from each item.
            The key must return a value supporting the < operator.

    Returns:
        New MetadataCollection with sorted items, or EMPTY if empty.

    Security:
        Key functions execute arbitrary code. Use only trusted sources.

    Examples:
        >>> coll = MetadataCollection(_items=(3, 1, 2))
        >>> list(coll.sorted())
        [1, 2, 3]
        >>> coll = MetadataCollection(_items=("b", "a", "c"))
        >>> list(coll.sorted())
        ['a', 'b', 'c']
        >>> # Custom key function
        >>> coll = MetadataCollection(_items=("bb", "a", "ccc"))
        >>> list(coll.sorted(key=len))
        ['a', 'bb', 'ccc']

    See Also:
        unique: Remove duplicates.
        reversed: Reverse order.
    """
    if not self._items:
        return MetadataCollection.EMPTY
    sort_key = key if key is not None else _default_sort_key
    sorted_items = tuple(builtins.sorted(self._items, key=sort_key))
    return MetadataCollection(_items=sorted_items)

reversed

reversed() -> MetadataCollection

Returns:

Type Description
MetadataCollection

New MetadataCollection with reversed items, or EMPTY if empty.

Examples:

>>> coll = MetadataCollection(_items=(1, 2, 3))
>>> list(coll.reversed())
[3, 2, 1]
>>> MetadataCollection.EMPTY.reversed() is MetadataCollection.EMPTY
True
Source code in src/typing_graph/_metadata.py
def reversed(self) -> "MetadataCollection":
    """Return collection with items in reverse order.

    Returns:
        New MetadataCollection with reversed items, or EMPTY if empty.

    Examples:
        >>> coll = MetadataCollection(_items=(1, 2, 3))
        >>> list(coll.reversed())
        [3, 2, 1]
        >>> MetadataCollection.EMPTY.reversed() is MetadataCollection.EMPTY
        True

    See Also:
        sorted: Sort items.
    """
    if not self._items:
        return MetadataCollection.EMPTY
    return MetadataCollection(_items=self._items[::-1])

map

map(func: Callable[[object], T]) -> tuple[T, ...]

Parameters:

Name Type Description Default
func Callable[[object], T]

Callable to apply to each item.

required

Returns:

Type Description
tuple[T, ...]

Tuple containing the results of applying func to each item.

Examples:

>>> coll = MetadataCollection(_items=(1, 2, 3))
>>> coll.map(lambda x: x * 2)
(2, 4, 6)
>>> coll = MetadataCollection(_items=("a", "bb", "ccc"))
>>> coll.map(len)
(1, 2, 3)
Source code in src/typing_graph/_metadata.py
def map(self, func: "Callable[[object], T]") -> tuple[T, ...]:
    """Apply a function to each item and return results as a tuple.

    This is a terminal operation - it returns a tuple, not a
    MetadataCollection, because the transformed values may not be
    valid metadata items.

    Args:
        func: Callable to apply to each item.

    Returns:
        Tuple containing the results of applying func to each item.

    Security:
        Functions execute arbitrary code. Use only trusted sources.

    Examples:
        >>> coll = MetadataCollection(_items=(1, 2, 3))
        >>> coll.map(lambda x: x * 2)
        (2, 4, 6)
        >>> coll = MetadataCollection(_items=("a", "bb", "ccc"))
        >>> coll.map(len)
        (1, 2, 3)

    See Also:
        partition: Split collection by predicate.
        filter: Keep items matching predicate.
    """
    # List comprehension is faster than generator expression inside tuple()
    return tuple([func(item) for item in self._items])

partition

partition(
    predicate: Callable[[object], bool],
) -> tuple[MetadataCollection, MetadataCollection]

Parameters:

Name Type Description Default
predicate Callable[[object], bool]

Callable taking an item, returning True if it should be in the first partition.

required

Returns:

Type Description
tuple[MetadataCollection, MetadataCollection]

Tuple of (matching, non_matching) MetadataCollections.

Examples:

>>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
>>> matching, non_matching = coll.partition(lambda x: x % 2 == 0)
>>> list(matching)
[2, 4]
>>> list(non_matching)
[1, 3, 5]
Source code in src/typing_graph/_metadata.py
def partition(
    self, predicate: "Callable[[object], bool]"
) -> tuple["MetadataCollection", "MetadataCollection"]:
    """Split collection into matching and non-matching items.

    Args:
        predicate: Callable taking an item, returning True if it should
            be in the first partition.

    Returns:
        Tuple of (matching, non_matching) MetadataCollections.

    Security:
        Predicates execute arbitrary code. Use only trusted sources.

    Examples:
        >>> coll = MetadataCollection(_items=(1, 2, 3, 4, 5))
        >>> matching, non_matching = coll.partition(lambda x: x % 2 == 0)
        >>> list(matching)
        [2, 4]
        >>> list(non_matching)
        [1, 3, 5]

    See Also:
        filter: Keep only matching items.
        map: Transform items.
    """
    matching: list[object] = []
    non_matching: list[object] = []
    for item in self._items:
        if predicate(item):
            matching.append(item)
        else:
            non_matching.append(item)

    matching_coll = (
        MetadataCollection(_items=tuple(matching))
        if matching
        else MetadataCollection.EMPTY
    )
    non_matching_coll = (
        MetadataCollection(_items=tuple(non_matching))
        if non_matching
        else MetadataCollection.EMPTY
    )
    return (matching_coll, non_matching_coll)

types

types() -> frozenset[type]

Returns:

Type Description
frozenset[type]

Frozenset containing the type of each unique item type.

Examples:

>>> coll = MetadataCollection(_items=("a", 1, "b", 2.0))
>>> sorted(t.__name__ for t in coll.types())
['float', 'int', 'str']
Source code in src/typing_graph/_metadata.py
def types(self) -> frozenset[type]:
    """Return the set of unique types in the collection.

    Returns:
        Frozenset containing the type of each unique item type.

    Examples:
        >>> coll = MetadataCollection(_items=("a", 1, "b", 2.0))
        >>> sorted(t.__name__ for t in coll.types())
        ['float', 'int', 'str']

    See Also:
        by_type: Group items by type.
    """
    return frozenset(type(item) for item in self._items)

by_type

by_type() -> Mapping[type, tuple[object, ...]]

Returns:

Type Description
Mapping[type, tuple[object, ...]]

Immutable mapping from type to tuple of items of that type.

Mapping[type, tuple[object, ...]]

Order within each group matches original insertion order.

Examples:

>>> coll = MetadataCollection(_items=("a", 1, "b", 2))
>>> grouped = coll.by_type()
>>> list(grouped[str])
['a', 'b']
>>> list(grouped[int])
[1, 2]
Source code in src/typing_graph/_metadata.py
def by_type(self) -> "Mapping[type, tuple[object, ...]]":
    """Group items by their type.

    Returns:
        Immutable mapping from type to tuple of items of that type.
        Order within each group matches original insertion order.

    Examples:
        >>> coll = MetadataCollection(_items=("a", 1, "b", 2))
        >>> grouped = coll.by_type()
        >>> list(grouped[str])
        ['a', 'b']
        >>> list(grouped[int])
        [1, 2]

    See Also:
        types: Get unique types only.
    """
    groups: dict[type, list[object]] = {}
    for item in self._items:
        item_type = type(item)
        if item_type not in groups:
            groups[item_type] = []
        groups[item_type].append(item)
    # Convert lists to tuples for immutability
    result: dict[type, tuple[object, ...]] = {
        k: tuple(v) for k, v in groups.items()
    }
    return MappingProxyType(result)

typing_graph.SupportsLessThan

Bases: Protocol

Methods:

Name Description
__lt__

Compare this object to another for less-than ordering.

Source code in src/typing_graph/_metadata.py
class SupportsLessThan(Protocol):
    """Protocol for types supporting the < operator.

    This protocol is used to type the `key` parameter in sorting operations,
    ensuring type safety while allowing any comparable type.
    """

    def __lt__(self, other: object, /) -> bool:
        """Compare this object to another for less-than ordering."""
        ...

__lt__

__lt__(other: object) -> bool
Source code in src/typing_graph/_metadata.py
def __lt__(self, other: object, /) -> bool:
    """Compare this object to another for less-than ordering."""
    ...

Edge types

Types representing edges between nodes in the type graph.

Classes:

Name Description
TypeEdge

Metadata describing a graph edge between nodes.

TypeEdgeKind

Semantic relationship between parent and child nodes.

TypeEdgeConnection

A connection from a node to a child node via an edge.

typing_graph.TypeEdge dataclass

Methods:

Name Description
field

Create a FIELD edge with the given name.

element

Create an ELEMENT edge with the given index.

Source code in src/typing_graph/_node.py
@dataclass(frozen=True, slots=True)
class TypeEdge:
    """Metadata describing a graph edge between nodes.

    Two TypeEdges are equal if they have the same (kind, name, index) tuple.
    """

    kind: TypeEdgeKind
    name: str | None = None
    index: int | None = None

    @override
    def __repr__(self) -> str:
        parts = [f"TypeEdgeKind.{self.kind.name}"]
        if self.name is not None:
            parts.append(f"name={self.name!r}")
        if self.index is not None:
            parts.append(f"index={self.index}")
        return f"TypeEdge({', '.join(parts)})"

    @classmethod
    def field(cls, name: str) -> "TypeEdge":
        """Create a FIELD edge with the given name."""
        return cls(TypeEdgeKind.FIELD, name=name)

    @classmethod
    def element(cls, index: int) -> "TypeEdge":
        """Create an ELEMENT edge with the given index."""
        return cls(TypeEdgeKind.ELEMENT, index=index)

field classmethod

field(name: str) -> TypeEdge
Source code in src/typing_graph/_node.py
@classmethod
def field(cls, name: str) -> "TypeEdge":
    """Create a FIELD edge with the given name."""
    return cls(TypeEdgeKind.FIELD, name=name)

element classmethod

element(index: int) -> TypeEdge
Source code in src/typing_graph/_node.py
@classmethod
def element(cls, index: int) -> "TypeEdge":
    """Create an ELEMENT edge with the given index."""
    return cls(TypeEdgeKind.ELEMENT, index=index)

typing_graph.TypeEdgeKind

Bases: str, Enum

Source code in src/typing_graph/_node.py
class TypeEdgeKind(str, Enum):
    """Semantic relationship between parent and child nodes."""

    # Structural/container edges
    ELEMENT = auto()  # tuple element (positional)
    KEY = auto()  # dict key
    VALUE = auto()  # dict value (dict[K, V] -> V)
    UNION_MEMBER = auto()  # union variant
    ALIAS_TARGET = auto()  # type alias target definition (type X = T -> T)
    INTERSECTION_MEMBER = auto()  # intersection member (Intersection[A, B] -> A, B)

    # Named/attribute edges
    FIELD = auto()  # class/typeddict field
    METHOD = auto()  # class method
    PARAM = auto()  # callable parameter
    RETURN = auto()  # callable return type

    # Secondary/meta-type edges
    ORIGIN = auto()  # The generic origin (list in list[int])
    BOUND = auto()  # TypeVar bound
    CONSTRAINT = auto()  # TypeVar constraint
    DEFAULT = auto()  # TypeParam default
    BASE = auto()  # Class base class
    TYPE_PARAM = auto()  # The TypeVar definition (Generic[T])
    TYPE_ARG = auto()  # The applied type argument (list[int])
    SIGNATURE = auto()  # Function -> Signature
    NARROWS = auto()  # TypeGuard/TypeIs target
    SUPERTYPE = auto()  # NewType supertype
    ANNOTATED_BASE = auto()  # Annotated[T, ...] -> T
    META_OF = auto()  # Type[T] -> T (the type being meta'd)
    TARGET = auto()  # Unpack[T] -> T (the unpacked type)
    PREFIX = auto()  # Concatenate[X, Y, P] -> X, Y (prefix types)
    PARAM_SPEC = auto()  # Concatenate[X, Y, P] -> P (the ParamSpec)
    RESOLVED = auto()  # ForwardRef -> resolved type (when resolved)
    VALUE_TYPE = auto()  # Enum -> value type (int, str, etc.)

typing_graph.TypeEdgeConnection dataclass

Source code in src/typing_graph/_node.py
@dataclass(frozen=True, slots=True)
class TypeEdgeConnection:
    """A connection from a node to a child node via an edge.

    TypeEdgeConnection provides a named alternative to tuple[TypeEdge, TypeNode],
    improving readability and IDE support for edge iteration.
    """

    edge: TypeEdge
    target: TypeNode

    @override
    def __repr__(self) -> str:
        return f"TypeEdgeConnection({self.edge!r} -> {self.target!r})"

Exceptions

Exception classes for error handling.

Base exception

Classes:

Name Description
TypingGraphError

Base exception for all typing-graph errors.

typing_graph.TypingGraphError

Bases: Exception

Source code in src/typing_graph/_exceptions.py
1
2
3
4
5
6
class TypingGraphError(Exception):
    """Base exception for all typing-graph errors.

    This is the root of the typing-graph exception hierarchy. All
    library-specific exceptions inherit from this class.
    """

Metadata exceptions

Classes:

Name Description
MetadataNotFoundError

Raised when requested metadata type is not found in a collection.

ProtocolNotRuntimeCheckableError

Raised when a protocol without @runtime_checkable is used for matching.

typing_graph.MetadataNotFoundError

Bases: LookupError

Attributes:

Name Type Description
requested_type type

The type that was searched for but not found.

collection MetadataCollection

The MetadataCollection that was searched.

Examples:

>>> coll = MetadataCollection()
>>> try:
...     raise MetadataNotFoundError(int, coll)
... except MetadataNotFoundError as e:
...     print(e.requested_type)
<class 'int'>

Methods:

Name Description
__init__

Initialize the exception with the requested type and collection.

Source code in src/typing_graph/_metadata.py
@final
class MetadataNotFoundError(LookupError):
    """Raised when requested metadata type is not found in a collection.

    This exception provides context about what type was requested and the
    collection that was searched, enabling better error messages and debugging.

    Attributes:
        requested_type: The type that was searched for but not found.
        collection: The MetadataCollection that was searched.

    Examples:
        >>> coll = MetadataCollection()
        >>> try:
        ...     raise MetadataNotFoundError(int, coll)
        ... except MetadataNotFoundError as e:
        ...     print(e.requested_type)
        <class 'int'>
    """

    requested_type: type
    collection: "MetadataCollection"

    def __init__(self, requested_type: type, collection: "MetadataCollection") -> None:
        """Initialize the exception with the requested type and collection.

        Args:
            requested_type: The type that was not found.
            collection: The collection that was searched.
        """
        self.requested_type = requested_type
        self.collection = collection
        msg = (
            f"No metadata of type {requested_type.__name__!r} found. "
            f"Use find() instead of get_required() if the type may not exist."
        )
        super().__init__(msg)
__init__
__init__(requested_type: type, collection: MetadataCollection) -> None

Parameters:

Name Type Description Default
requested_type type

The type that was not found.

required
collection MetadataCollection

The collection that was searched.

required
Source code in src/typing_graph/_metadata.py
def __init__(self, requested_type: type, collection: "MetadataCollection") -> None:
    """Initialize the exception with the requested type and collection.

    Args:
        requested_type: The type that was not found.
        collection: The collection that was searched.
    """
    self.requested_type = requested_type
    self.collection = collection
    msg = (
        f"No metadata of type {requested_type.__name__!r} found. "
        f"Use find() instead of get_required() if the type may not exist."
    )
    super().__init__(msg)

typing_graph.ProtocolNotRuntimeCheckableError

Bases: TypeError

Attributes:

Name Type Description
protocol type

The protocol type that is not runtime checkable.

Examples:

>>> from typing import Protocol
>>> class NotRuntime(Protocol):
...     value: int
>>> try:
...     raise ProtocolNotRuntimeCheckableError(NotRuntime)
... except ProtocolNotRuntimeCheckableError as e:
...     print(e.protocol.__name__)
NotRuntime

Methods:

Name Description
__init__

Initialize the exception with the non-runtime-checkable protocol.

Source code in src/typing_graph/_metadata.py
@final
class ProtocolNotRuntimeCheckableError(TypeError):
    """Raised when a protocol without @runtime_checkable is used for matching.

    Protocol-based methods like find_protocol() and has_protocol() require
    the protocol to be decorated with @runtime_checkable. This exception
    provides clear guidance on how to fix the error.

    Attributes:
        protocol: The protocol type that is not runtime checkable.

    Examples:
        >>> from typing import Protocol
        >>> class NotRuntime(Protocol):
        ...     value: int
        >>> try:
        ...     raise ProtocolNotRuntimeCheckableError(NotRuntime)
        ... except ProtocolNotRuntimeCheckableError as e:
        ...     print(e.protocol.__name__)
        NotRuntime
    """

    protocol: type

    def __init__(self, protocol: type) -> None:
        """Initialize the exception with the non-runtime-checkable protocol.

        Args:
            protocol: The protocol that is not runtime checkable.
        """
        self.protocol = protocol
        msg = (
            f"{protocol.__name__} is not @runtime_checkable. "
            "Add @runtime_checkable decorator to use with "
            "find_protocol() or has_protocol()."
        )
        super().__init__(msg)
__init__
__init__(protocol: type) -> None

Parameters:

Name Type Description Default
protocol type

The protocol that is not runtime checkable.

required
Source code in src/typing_graph/_metadata.py
def __init__(self, protocol: type) -> None:
    """Initialize the exception with the non-runtime-checkable protocol.

    Args:
        protocol: The protocol that is not runtime checkable.
    """
    self.protocol = protocol
    msg = (
        f"{protocol.__name__} is not @runtime_checkable. "
        "Add @runtime_checkable decorator to use with "
        "find_protocol() or has_protocol()."
    )
    super().__init__(msg)

Traversal exceptions

Classes:

Name Description
TraversalError

Error during graph traversal.

typing_graph.TraversalError

Bases: TypingGraphError

Source code in src/typing_graph/_exceptions.py
class TraversalError(TypingGraphError):
    """Error during graph traversal.

    Raised when traversal encounters an unrecoverable error condition:
    - max_depth is negative (invalid parameter)
    - Implementation errors detected during traversal
    """

Type guards

Node type guards

Type guard functions for type narrowing on node types.

Functions:

Name Description
is_type_node

Return whether the argument is an instance of TypeNode.

is_concrete_node

Return whether the argument is a ConcreteNode instance.

is_annotated_node

Return whether the argument is an AnnotatedNode instance.

is_subscripted_generic_node

Return whether the argument is a SubscriptedGenericNode instance.

is_generic_alias_node

Return whether the argument is a GenericAliasNode instance.

is_generic_node

Return whether the argument is a GenericType instance.

is_union_type_node

Return whether the argument is a UnionNode instance.

is_intersection_node

Return whether the argument is an IntersectionNode instance.

is_type_var_node

Return whether the argument is a TypeVarNode instance.

is_param_spec_node

Return whether the argument is a ParamSpecNode instance.

is_type_var_tuple_node

Return whether the argument is a TypeVarTupleNode instance.

is_type_param_node

Check if a node is a type parameter (TypeVar, ParamSpec, or TypeVarTuple).

is_any_node

Return whether the argument is an AnyNode instance.

is_never_node

Return whether the argument is a NeverNode instance.

is_self_node

Return whether the argument is a SelfNode instance.

is_literal_node

Return whether the argument is a LiteralNode instance.

is_literal_string_node

Return whether the argument is a LiteralStringNode instance.

is_tuple_node

Return whether the argument is a TupleNode instance.

is_ellipsis_node

Return whether the argument is an EllipsisNode instance.

is_forward_ref_node

Return whether the argument is a ForwardRefNode instance.

is_meta_node

Return whether the argument is a MetaNode instance.

is_concatenate_node

Return whether the argument is a ConcatenateNode instance.

is_unpack_node

Return whether the argument is an UnpackNode instance.

is_type_guard_node

Return whether the argument is a TypeGuardNode instance.

is_type_is_node

Return whether the argument is a TypeIsNode instance.

is_ref_state_resolved

Return whether the argument is a RefResolved instance.

is_ref_state_unresolved

Return whether the argument is a RefUnresolved instance.

is_ref_state_failed

Return whether the argument is a RefFailed instance.

is_structured_node

Return whether the argument is a StructuredNode instance.

is_class_node

Return whether the argument is a ClassNode instance.

is_dataclass_node

Return whether the argument is a DataclassNode instance.

is_typed_dict_node

Return whether the argument is a TypedDictNode instance.

is_named_tuple_node

Return whether the argument is a NamedTupleNode instance.

is_enum_node

Return whether the argument is an EnumNode instance.

is_protocol_node

Return whether the argument is a ProtocolNode instance.

is_function_node

Return whether the argument is a FunctionNode instance.

is_callable_node

Return whether the argument is a CallableNode instance.

is_signature_node

Return whether the argument is a SignatureNode instance.

is_method_sig

Return whether the argument is a MethodSig instance.

is_type_alias_node

Return whether the argument is a TypeAliasNode instance.

is_new_type_node

Return whether the argument is a NewTypeNode instance.

typing_graph.is_type_node

is_type_node(obj: object) -> TypeIs[TypeNode]
Source code in src/typing_graph/_node.py
def is_type_node(obj: object) -> TypeIs[TypeNode]:
    """Return whether the argument is an instance of TypeNode."""
    return isinstance(obj, TypeNode)

typing_graph.is_concrete_node

is_concrete_node(obj: object) -> TypeIs[ConcreteNode]
Source code in src/typing_graph/_node.py
def is_concrete_node(obj: object) -> TypeIs[ConcreteNode]:
    """Return whether the argument is a ConcreteNode instance."""
    return isinstance(obj, ConcreteNode)

typing_graph.is_annotated_node

is_annotated_node(obj: object) -> TypeIs[AnnotatedNode]
Source code in src/typing_graph/_node.py
def is_annotated_node(obj: object) -> TypeIs[AnnotatedNode]:
    """Return whether the argument is an AnnotatedNode instance."""
    return isinstance(obj, AnnotatedNode)

typing_graph.is_subscripted_generic_node

is_subscripted_generic_node(obj: object) -> TypeIs[SubscriptedGenericNode]
Source code in src/typing_graph/_node.py
def is_subscripted_generic_node(obj: object) -> TypeIs[SubscriptedGenericNode]:
    """Return whether the argument is a SubscriptedGenericNode instance."""
    return isinstance(obj, SubscriptedGenericNode)

typing_graph.is_generic_alias_node

is_generic_alias_node(obj: object) -> TypeIs[GenericAliasNode]
Source code in src/typing_graph/_node.py
def is_generic_alias_node(obj: object) -> TypeIs[GenericAliasNode]:
    """Return whether the argument is a GenericAliasNode instance."""
    return isinstance(obj, GenericAliasNode)

typing_graph.is_generic_node

is_generic_node(obj: object) -> TypeIs[GenericTypeNode]
Source code in src/typing_graph/_node.py
def is_generic_node(obj: object) -> TypeIs[GenericTypeNode]:
    """Return whether the argument is a GenericType instance."""
    return isinstance(obj, GenericTypeNode)

typing_graph.is_union_type_node

is_union_type_node(obj: object) -> TypeIs[UnionNode]
Source code in src/typing_graph/_node.py
def is_union_type_node(obj: object) -> TypeIs[UnionNode]:
    """Return whether the argument is a UnionNode instance."""
    return isinstance(obj, UnionNode)

typing_graph.is_intersection_node

is_intersection_node(obj: object) -> TypeIs[IntersectionNode]
Source code in src/typing_graph/_node.py
def is_intersection_node(obj: object) -> TypeIs[IntersectionNode]:
    """Return whether the argument is an IntersectionNode instance."""
    return isinstance(obj, IntersectionNode)

typing_graph.is_type_var_node

is_type_var_node(obj: object) -> TypeIs[TypeVarNode]
Source code in src/typing_graph/_node.py
def is_type_var_node(obj: object) -> TypeIs[TypeVarNode]:
    """Return whether the argument is a TypeVarNode instance."""
    return isinstance(obj, TypeVarNode)

typing_graph.is_param_spec_node

is_param_spec_node(obj: object) -> TypeIs[ParamSpecNode]
Source code in src/typing_graph/_node.py
def is_param_spec_node(obj: object) -> TypeIs[ParamSpecNode]:
    """Return whether the argument is a ParamSpecNode instance."""
    return isinstance(obj, ParamSpecNode)

typing_graph.is_type_var_tuple_node

is_type_var_tuple_node(obj: object) -> TypeIs[TypeVarTupleNode]
Source code in src/typing_graph/_node.py
def is_type_var_tuple_node(obj: object) -> TypeIs[TypeVarTupleNode]:
    """Return whether the argument is a TypeVarTupleNode instance."""
    return isinstance(obj, TypeVarTupleNode)

typing_graph.is_type_param_node

is_type_param_node(node: TypeNode) -> TypeIs[TypeParamNode]

Parameters:

Name Type Description Default
node TypeNode

The TypeNode to check.

required

Returns:

Type Description
TypeIs[TypeParamNode]

True if the node is a TypeVarNode, ParamSpecNode, or TypeVarTupleNode.

Source code in src/typing_graph/_node.py
def is_type_param_node(node: TypeNode) -> TypeIs[TypeParamNode]:
    """Check if a node is a type parameter (TypeVar, ParamSpec, or TypeVarTuple).

    Args:
        node: The TypeNode to check.

    Returns:
        True if the node is a TypeVarNode, ParamSpecNode, or TypeVarTupleNode.
    """
    return (
        is_type_var_node(node)
        or is_param_spec_node(node)
        or is_type_var_tuple_node(node)
    )

typing_graph.is_any_node

is_any_node(obj: object) -> TypeIs[AnyNode]
Source code in src/typing_graph/_node.py
def is_any_node(obj: object) -> TypeIs[AnyNode]:
    """Return whether the argument is an AnyNode instance."""
    return isinstance(obj, AnyNode)

typing_graph.is_never_node

is_never_node(obj: object) -> TypeIs[NeverNode]
Source code in src/typing_graph/_node.py
def is_never_node(obj: object) -> TypeIs[NeverNode]:
    """Return whether the argument is a NeverNode instance."""
    return isinstance(obj, NeverNode)

typing_graph.is_self_node

is_self_node(obj: object) -> TypeIs[SelfNode]
Source code in src/typing_graph/_node.py
def is_self_node(obj: object) -> TypeIs[SelfNode]:
    """Return whether the argument is a SelfNode instance."""
    return isinstance(obj, SelfNode)

typing_graph.is_literal_node

is_literal_node(obj: object) -> TypeIs[LiteralNode]
Source code in src/typing_graph/_node.py
def is_literal_node(obj: object) -> TypeIs[LiteralNode]:
    """Return whether the argument is a LiteralNode instance."""
    return isinstance(obj, LiteralNode)

typing_graph.is_literal_string_node

is_literal_string_node(obj: object) -> TypeIs[LiteralStringNode]
Source code in src/typing_graph/_node.py
def is_literal_string_node(obj: object) -> TypeIs[LiteralStringNode]:
    """Return whether the argument is a LiteralStringNode instance."""
    return isinstance(obj, LiteralStringNode)

typing_graph.is_tuple_node

is_tuple_node(obj: object) -> TypeIs[TupleNode]
Source code in src/typing_graph/_node.py
def is_tuple_node(obj: object) -> TypeIs[TupleNode]:
    """Return whether the argument is a TupleNode instance."""
    return isinstance(obj, TupleNode)

typing_graph.is_ellipsis_node

is_ellipsis_node(obj: object) -> TypeIs[EllipsisNode]
Source code in src/typing_graph/_node.py
def is_ellipsis_node(obj: object) -> TypeIs[EllipsisNode]:
    """Return whether the argument is an EllipsisNode instance."""
    return isinstance(obj, EllipsisNode)

typing_graph.is_forward_ref_node

is_forward_ref_node(obj: object) -> TypeIs[ForwardRefNode]
Source code in src/typing_graph/_node.py
def is_forward_ref_node(obj: object) -> TypeIs[ForwardRefNode]:
    """Return whether the argument is a ForwardRefNode instance."""
    return isinstance(obj, ForwardRefNode)

typing_graph.is_meta_node

is_meta_node(obj: object) -> TypeIs[MetaNode]
Source code in src/typing_graph/_node.py
def is_meta_node(obj: object) -> TypeIs[MetaNode]:
    """Return whether the argument is a MetaNode instance."""
    return isinstance(obj, MetaNode)

typing_graph.is_concatenate_node

is_concatenate_node(obj: object) -> TypeIs[ConcatenateNode]
Source code in src/typing_graph/_node.py
def is_concatenate_node(obj: object) -> TypeIs[ConcatenateNode]:
    """Return whether the argument is a ConcatenateNode instance."""
    return isinstance(obj, ConcatenateNode)

typing_graph.is_unpack_node

is_unpack_node(obj: object) -> TypeIs[UnpackNode]
Source code in src/typing_graph/_node.py
def is_unpack_node(obj: object) -> TypeIs[UnpackNode]:
    """Return whether the argument is an UnpackNode instance."""
    return isinstance(obj, UnpackNode)

typing_graph.is_type_guard_node

is_type_guard_node(obj: object) -> TypeIs[TypeGuardNode]
Source code in src/typing_graph/_node.py
def is_type_guard_node(obj: object) -> TypeIs[TypeGuardNode]:
    """Return whether the argument is a TypeGuardNode instance."""
    return isinstance(obj, TypeGuardNode)

typing_graph.is_type_is_node

is_type_is_node(obj: object) -> TypeIs[TypeIsNode]
Source code in src/typing_graph/_node.py
def is_type_is_node(obj: object) -> TypeIs[TypeIsNode]:
    """Return whether the argument is a TypeIsNode instance."""
    return isinstance(obj, TypeIsNode)

typing_graph.is_ref_state_resolved

is_ref_state_resolved(state: object) -> TypeIs[RefResolved]
Source code in src/typing_graph/_node.py
def is_ref_state_resolved(state: object) -> TypeIs[RefResolved]:
    """Return whether the argument is a RefResolved instance."""
    return isinstance(state, RefResolved)

typing_graph.is_ref_state_unresolved

is_ref_state_unresolved(state: object) -> TypeIs[RefUnresolved]
Source code in src/typing_graph/_node.py
def is_ref_state_unresolved(state: object) -> TypeIs[RefUnresolved]:
    """Return whether the argument is a RefUnresolved instance."""
    return isinstance(state, RefUnresolved)

typing_graph.is_ref_state_failed

is_ref_state_failed(state: object) -> TypeIs[RefFailed]
Source code in src/typing_graph/_node.py
def is_ref_state_failed(state: object) -> TypeIs[RefFailed]:
    """Return whether the argument is a RefFailed instance."""
    return isinstance(state, RefFailed)

typing_graph.is_structured_node

is_structured_node(obj: object) -> TypeIs[StructuredNode]
Source code in src/typing_graph/_node.py
def is_structured_node(obj: object) -> TypeIs[StructuredNode]:
    """Return whether the argument is a StructuredNode instance."""
    return isinstance(obj, StructuredNode)

typing_graph.is_class_node

is_class_node(obj: object) -> TypeIs[ClassNode]
Source code in src/typing_graph/_node.py
def is_class_node(obj: object) -> TypeIs[ClassNode]:
    """Return whether the argument is a ClassNode instance."""
    return isinstance(obj, ClassNode)

typing_graph.is_dataclass_node

is_dataclass_node(obj: object) -> TypeIs[DataclassNode]
Source code in src/typing_graph/_node.py
def is_dataclass_node(obj: object) -> TypeIs[DataclassNode]:
    """Return whether the argument is a DataclassNode instance."""
    return isinstance(obj, DataclassNode)

typing_graph.is_typed_dict_node

is_typed_dict_node(obj: object) -> TypeIs[TypedDictNode]
Source code in src/typing_graph/_node.py
def is_typed_dict_node(obj: object) -> TypeIs[TypedDictNode]:
    """Return whether the argument is a TypedDictNode instance."""
    return isinstance(obj, TypedDictNode)

typing_graph.is_named_tuple_node

is_named_tuple_node(obj: object) -> TypeIs[NamedTupleNode]
Source code in src/typing_graph/_node.py
def is_named_tuple_node(obj: object) -> TypeIs[NamedTupleNode]:
    """Return whether the argument is a NamedTupleNode instance."""
    return isinstance(obj, NamedTupleNode)

typing_graph.is_enum_node

is_enum_node(obj: object) -> TypeIs[EnumNode]
Source code in src/typing_graph/_node.py
def is_enum_node(obj: object) -> TypeIs[EnumNode]:
    """Return whether the argument is an EnumNode instance."""
    return isinstance(obj, EnumNode)

typing_graph.is_protocol_node

is_protocol_node(obj: object) -> TypeIs[ProtocolNode]
Source code in src/typing_graph/_node.py
def is_protocol_node(obj: object) -> TypeIs[ProtocolNode]:
    """Return whether the argument is a ProtocolNode instance."""
    return isinstance(obj, ProtocolNode)

typing_graph.is_function_node

is_function_node(obj: object) -> TypeIs[FunctionNode]
Source code in src/typing_graph/_node.py
def is_function_node(obj: object) -> TypeIs[FunctionNode]:
    """Return whether the argument is a FunctionNode instance."""
    return isinstance(obj, FunctionNode)

typing_graph.is_callable_node

is_callable_node(obj: object) -> TypeIs[CallableNode]
Source code in src/typing_graph/_node.py
def is_callable_node(obj: object) -> TypeIs[CallableNode]:
    """Return whether the argument is a CallableNode instance."""
    return isinstance(obj, CallableNode)

typing_graph.is_signature_node

is_signature_node(obj: object) -> TypeIs[SignatureNode]
Source code in src/typing_graph/_node.py
def is_signature_node(obj: object) -> TypeIs[SignatureNode]:
    """Return whether the argument is a SignatureNode instance."""
    return isinstance(obj, SignatureNode)

typing_graph.is_method_sig

is_method_sig(obj: object) -> TypeIs[MethodSig]
Source code in src/typing_graph/_node.py
def is_method_sig(obj: object) -> TypeIs[MethodSig]:
    """Return whether the argument is a MethodSig instance."""
    return isinstance(obj, MethodSig)

typing_graph.is_type_alias_node

is_type_alias_node(obj: object) -> TypeIs[TypeAliasNode]
Source code in src/typing_graph/_node.py
def is_type_alias_node(obj: object) -> TypeIs[TypeAliasNode]:
    """Return whether the argument is a TypeAliasNode instance."""
    return isinstance(obj, TypeAliasNode)

typing_graph.is_new_type_node

is_new_type_node(obj: object) -> TypeIs[NewTypeNode]
Source code in src/typing_graph/_node.py
def is_new_type_node(obj: object) -> TypeIs[NewTypeNode]:
    """Return whether the argument is a NewTypeNode instance."""
    return isinstance(obj, NewTypeNode)

Class detection functions

Functions for detecting special class types at runtime.

Functions:

Name Description
is_dataclass_class

Check if cls is a dataclass.

is_typeddict_class

Check if cls is a TypedDict.

is_namedtuple_class

Check if cls is a NamedTuple.

is_enum_class

Check if cls is an Enum subclass with TypeIs narrowing.

is_protocol_class

Check if cls is a Protocol.

typing_graph.is_dataclass_class

is_dataclass_class(cls: type[Any]) -> bool
Source code in src/typing_graph/_inspect_class.py
def is_dataclass_class(cls: type[Any]) -> bool:
    """Check if cls is a dataclass.

    Checks that cls is both a dataclass and a type (not an instance).

    Note:
        Returns ``bool`` rather than ``TypeIs`` because dataclass inspection
        functions work with the general ``type`` parameter.
    """
    return dataclasses.is_dataclass(cls)

typing_graph.is_typeddict_class

is_typeddict_class(cls: type[Any]) -> bool
Source code in src/typing_graph/_inspect_class.py
def is_typeddict_class(cls: type[Any]) -> bool:
    """Check if cls is a TypedDict.

    Note:
        Returns ``bool`` rather than ``TypeIs`` because TypedDict inspection
        functions work with the general ``type`` parameter.
    """
    return _is_typeddict(cls)

typing_graph.is_namedtuple_class

is_namedtuple_class(cls: type[Any]) -> bool
Source code in src/typing_graph/_inspect_class.py
def is_namedtuple_class(cls: type[Any]) -> bool:
    """Check if cls is a NamedTuple.

    Note:
        Returns ``bool`` rather than ``TypeIs`` because NamedTuple inspection
        functions work with the general ``type`` parameter.
    """
    return is_namedtuple(cls)

typing_graph.is_enum_class

is_enum_class(cls: type[Any]) -> TypeIs[type[Enum]]
Source code in src/typing_graph/_inspect_class.py
def is_enum_class(cls: type[Any]) -> TypeIs[type[Enum]]:
    """Check if cls is an Enum subclass with TypeIs narrowing.

    Unlike the other ``is_*_class`` functions, this returns ``TypeIs[type[Enum]]``
    rather than ``bool``. This provides type narrowing so that ``_inspect_enum``
    receives ``type[Enum]`` as required by its signature.

    Note:
        The other class detection functions return ``bool`` because their
        corresponding inspection functions accept the general ``type`` parameter.
        Only ``_inspect_enum`` requires the narrowed ``type[Enum]`` type.
    """
    try:
        return issubclass(cls, Enum)
    except TypeError:
        return False

typing_graph.is_protocol_class

is_protocol_class(cls: type[Any]) -> bool
Source code in src/typing_graph/_inspect_class.py
def is_protocol_class(cls: type[Any]) -> bool:
    """Check if cls is a Protocol.

    Note:
        Returns ``bool`` rather than ``TypeIs`` because Protocol inspection
        functions work with the general ``type`` parameter.
    """
    return is_protocol(cls)

Utility functions

Helper functions for working with union types and optional values.

Functions:

Name Description
get_union_members

Extract union members from either union representation.

is_union_node

Check if a node represents any union type.

is_optional_node

Check if a node represents an optional type (union containing None).

unwrap_optional

Extract non-None types from an optional union.

to_runtime_type

Convert a TypeNode back to runtime type hints.

typing_graph.get_union_members

get_union_members(node: TypeNode) -> tuple[TypeNode, ...] | None

Parameters:

Name Type Description Default
node TypeNode

A TypeNode to extract union members from.

required

Returns:

Type Description
tuple[TypeNode, ...] | None

A tuple of member TypeNodes if node represents a union type,

tuple[TypeNode, ...] | None

or None if node is not a union.

Examples:

>>> from typing import Literal
>>> from typing_graph import inspect_type, get_union_members
>>>
>>> # types.UnionType (PEP 604 with concrete types)
>>> node1 = inspect_type(int | str)
>>> members1 = get_union_members(node1)
>>> len(members1)
2
>>>
>>> # typing.Union (from Literal | Literal)
>>> node2 = inspect_type(Literal["a"] | Literal["b"])
>>> members2 = get_union_members(node2)
>>> len(members2)
2
Source code in src/typing_graph/_helpers.py
def get_union_members(node: TypeNode) -> tuple[TypeNode, ...] | None:
    """Extract union members from either union representation.

    Python has two runtime representations for union types:

    - ``types.UnionType`` (PEP 604 ``|`` with concrete types) → ``UnionNode``
    - ``typing.Union`` (``Union[...]`` or ``|`` with typing special forms) →
      ``SubscriptedGeneric`` with ``origin.cls=typing.Union``

    This function handles both, returning the member types as a tuple.

    This function works consistently regardless of the ``normalize_unions``
    configuration setting, providing uniform access to union members whether
    the union is represented as ``UnionNode`` or ``SubscriptedGenericNode``.

    Args:
        node: A TypeNode to extract union members from.

    Returns:
        A tuple of member TypeNodes if ``node`` represents a union type,
        or ``None`` if ``node`` is not a union.

    Examples:
        >>> from typing import Literal
        >>> from typing_graph import inspect_type, get_union_members
        >>>
        >>> # types.UnionType (PEP 604 with concrete types)
        >>> node1 = inspect_type(int | str)
        >>> members1 = get_union_members(node1)
        >>> len(members1)
        2
        >>>
        >>> # typing.Union (from Literal | Literal)
        >>> node2 = inspect_type(Literal["a"] | Literal["b"])
        >>> members2 = get_union_members(node2)
        >>> len(members2)
        2

    Note:
        Use [is_union_node][typing_graph.is_union_node] to check if a node is
        a union before calling this function.
    """
    if not is_union_node(node):
        return None

    if is_union_type_node(node):
        return node.members

    # Must be SubscriptedGeneric with typing.Union origin (checked by is_union)
    if is_subscripted_generic_node(node):
        return node.args

    return None  # pragma: no cover

typing_graph.is_union_node

is_union_node(node: TypeNode) -> bool

Parameters:

Name Type Description Default
node TypeNode

A TypeNode to check.

required

Returns:

Type Description
bool

True if the node represents a union type, False otherwise.

Examples:

>>> from typing import Literal
>>> from typing_graph import inspect_type, is_union_node
>>>
>>> is_union_node(inspect_type(int | str))
True
>>> is_union_node(inspect_type(Literal["a"] | Literal["b"]))
True
>>> is_union_node(inspect_type(list[int]))
False
Source code in src/typing_graph/_helpers.py
def is_union_node(node: TypeNode) -> bool:
    """Check if a node represents any union type.

    Returns ``True`` for both union representations:

    - ``UnionNode`` (from ``types.UnionType``, e.g., ``int | str``)
    - ``SubscriptedGeneric`` with ``origin.cls=typing.Union``
      (e.g., ``Literal['a'] | Literal['b']``)

    This function works consistently regardless of the ``normalize_unions``
    configuration setting. When normalization is enabled (the default),
    unions become ``UnionNode``; when disabled, ``typing.Union`` becomes
    ``SubscriptedGenericNode``. This function detects both.

    Args:
        node: A TypeNode to check.

    Returns:
        ``True`` if the node represents a union type, ``False`` otherwise.

    Examples:
        >>> from typing import Literal
        >>> from typing_graph import inspect_type, is_union_node
        >>>
        >>> is_union_node(inspect_type(int | str))
        True
        >>> is_union_node(inspect_type(Literal["a"] | Literal["b"]))
        True
        >>> is_union_node(inspect_type(list[int]))
        False

    Note:
        Use [get_union_members][typing_graph.get_union_members] to extract the
        member types. Use [is_union_type_node][typing_graph.is_union_type_node]
        if you only want to match ``UnionNode`` (not ``typing.Union``).
    """
    if is_union_type_node(node):
        return True

    if is_subscripted_generic_node(node):
        origin = node.origin
        # Checking identity against typing.Union for runtime introspection,
        # not using it as a type annotation, so the deprecation doesn't apply.
        if isinstance(origin, GenericTypeNode) and origin.cls is typing.Union:  # pyright: ignore[reportDeprecated]
            return True

    return False

typing_graph.is_optional_node

is_optional_node(node: TypeNode) -> bool

Parameters:

Name Type Description Default
node TypeNode

A TypeNode to check.

required

Returns:

Type Description
bool

True if the node is a union containing None, False otherwise.

Examples:

>>> from typing import Optional, Union
>>> from typing_graph import inspect_type, is_optional_node
>>>
>>> is_optional_node(inspect_type(int | None))
True
>>> is_optional_node(inspect_type(Optional[str]))
True
>>> is_optional_node(inspect_type(int | str))
False
>>> is_optional_node(inspect_type(int))
False
Source code in src/typing_graph/_helpers.py
def is_optional_node(node: TypeNode) -> bool:
    """Check if a node represents an optional type (union containing None).

    Returns ``True`` for any union that includes ``None`` as a member:

    - ``int | None`` (PEP 604 syntax)
    - ``Union[int, None]`` (typing.Union)
    - ``Optional[int]`` (equivalent to Union[int, None])

    This function works consistently regardless of the ``normalize_unions``
    configuration setting.

    Args:
        node: A TypeNode to check.

    Returns:
        ``True`` if the node is a union containing ``None``, ``False`` otherwise.

    Examples:
        >>> from typing import Optional, Union
        >>> from typing_graph import inspect_type, is_optional_node
        >>>
        >>> is_optional_node(inspect_type(int | None))
        True
        >>> is_optional_node(inspect_type(Optional[str]))
        True
        >>> is_optional_node(inspect_type(int | str))
        False
        >>> is_optional_node(inspect_type(int))
        False

    Note:
        Use [unwrap_optional][typing_graph.unwrap_optional] to extract the
        non-None type(s) from an optional.
    """
    members = get_union_members(node)
    if members is None:
        return False

    return any(is_concrete_node(m) and m.cls is type(None) for m in members)

typing_graph.unwrap_optional

unwrap_optional(node: TypeNode) -> tuple[TypeNode, ...] | None

Parameters:

Name Type Description Default
node TypeNode

A TypeNode to unwrap.

required

Returns:

Type Description
tuple[TypeNode, ...] | None

A tuple of non-None member TypeNodes if node is an optional,

tuple[TypeNode, ...] | None

or None if node is not an optional.

Examples:

>>> from typing import Optional
>>> from typing_graph import inspect_type, unwrap_optional, is_concrete_node
>>>
>>> node = inspect_type(int | None)
>>> unwrapped = unwrap_optional(node)
>>> len(unwrapped)
1
>>> is_concrete_node(unwrapped[0]) and unwrapped[0].cls is int
True
>>>
>>> # Multiple non-None types
>>> node2 = inspect_type(int | str | None)
>>> unwrapped2 = unwrap_optional(node2)
>>> len(unwrapped2)
2
>>>
>>> # Not an optional
>>> unwrap_optional(inspect_type(int | str)) is None
True
Source code in src/typing_graph/_helpers.py
def unwrap_optional(node: TypeNode) -> tuple[TypeNode, ...] | None:
    """Extract non-None types from an optional union.

    Given an optional type (a union containing ``None``), returns a tuple of
    the non-None member types. Returns ``None`` if the node is not an optional.

    This function works consistently regardless of the ``normalize_unions``
    configuration setting.

    Args:
        node: A TypeNode to unwrap.

    Returns:
        A tuple of non-None member TypeNodes if ``node`` is an optional,
        or ``None`` if ``node`` is not an optional.

    Examples:
        >>> from typing import Optional
        >>> from typing_graph import inspect_type, unwrap_optional, is_concrete_node
        >>>
        >>> node = inspect_type(int | None)
        >>> unwrapped = unwrap_optional(node)
        >>> len(unwrapped)
        1
        >>> is_concrete_node(unwrapped[0]) and unwrapped[0].cls is int
        True
        >>>
        >>> # Multiple non-None types
        >>> node2 = inspect_type(int | str | None)
        >>> unwrapped2 = unwrap_optional(node2)
        >>> len(unwrapped2)
        2
        >>>
        >>> # Not an optional
        >>> unwrap_optional(inspect_type(int | str)) is None
        True

    Note:
        Use [is_optional_node][typing_graph.is_optional_node] to check if a
        node is optional before calling this function.
    """
    if not is_optional_node(node):
        return None

    members = get_union_members(node)
    if members is None:  # pragma: no cover
        return None

    return tuple(
        m for m in members if not (is_concrete_node(m) and m.cls is type(None))
    )

typing_graph.to_runtime_type

to_runtime_type(node: TypeNode, *, include_extras: bool = True) -> Any

Parameters:

Name Type Description Default
node TypeNode

The TypeNode to convert.

required
include_extras bool

Whether to include metadata as Annotated (default True).

True

Returns:

Type Description
Any

A runtime type annotation corresponding to the node.

Raises:

Type Description
TypeError

If the node is a TypeVarNode, ParamSpecNode, TypeVarTupleNode, or a CallableNode with ParamSpec parameters. These types cannot be reconstructed because the original TypeVar/ParamSpec objects are not preserved.

Source code in src/typing_graph/_inspect_type.py
def to_runtime_type(  # noqa: PLR0912, PLR0915 - Inherently complex type dispatch
    node: TypeNode,
    *,
    include_extras: bool = True,
) -> Any:
    """Convert a TypeNode back to runtime type hints.

    This is the reverse operation of inspect_type.

    Args:
        node: The TypeNode to convert.
        include_extras: Whether to include metadata as Annotated (default True).

    Returns:
        A runtime type annotation corresponding to the node.

    Raises:
        TypeError: If the node is a TypeVarNode, ParamSpecNode,
            TypeVarTupleNode, or a CallableNode with ParamSpec parameters.
            These types cannot be reconstructed because the original
            TypeVar/ParamSpec objects are not preserved.
    """
    result: Any

    if is_concrete_node(node) or is_generic_node(node):
        result = node.cls
    elif is_any_node(node):
        result = Any
    elif is_never_node(node):
        result = Never
    elif is_self_node(node):
        result = Self
    elif is_meta_node(node):
        inner = to_runtime_type(node.of)
        result = type[inner]
    elif is_literal_node(node):
        result = Literal[node.values]
    elif is_union_type_node(node):
        member_types = tuple(to_runtime_type(m) for m in node.members)
        result = functools.reduce(operator.or_, member_types)
    elif is_subscripted_generic_node(node):
        origin = to_runtime_type(node.origin)
        args = tuple(to_runtime_type(a) for a in node.args)
        result = origin[args] if args else origin
    elif is_tuple_node(node):
        if node.homogeneous:
            elem = to_runtime_type(node.elements[0])
            # tuple[T, ...] for homogeneous
            result = tuple.__class_getitem__((elem, ...))
        elif not node.elements:
            # tuple[()] for empty tuple
            result = tuple.__class_getitem__(())
        else:
            elems = tuple(to_runtime_type(e) for e in node.elements)
            result = tuple.__class_getitem__(elems)
    elif is_callable_node(node):
        returns = to_runtime_type(node.returns)
        if isinstance(node.params, tuple):
            params = [to_runtime_type(p) for p in node.params]
            # __class_getitem__ exists at runtime but isn't in type stubs;
            # using getattr avoids type errors (noqa: B009 - intentional)
            class_getitem = getattr(Callable, "__class_getitem__")  # noqa: B009
            result = class_getitem((params, returns))
        elif is_ellipsis_node(node.params):
            result = Callable[..., returns]
        else:
            # ParamSpec or Concatenate can't be recreated at runtime without the
            # original ParamSpec object, which we don't have.
            params_type = type(node.params).__name__
            msg = f"Cannot convert {params_type} back to runtime type hint"
            raise TypeError(msg)
    elif (
        is_type_var_node(node)
        or is_param_spec_node(node)
        or is_type_var_tuple_node(node)
    ):
        # TypeVar, ParamSpec, and TypeVarTuple can't be recreated without the
        # original object, which we don't have.
        msg = f"Cannot convert {type(node).__name__} back to runtime type hint"
        raise TypeError(msg)
    elif is_forward_ref_node(node):
        if is_ref_state_resolved(node.state):
            result = to_runtime_type(node.state.node)
        else:
            result = TypingForwardRef(node.ref)
    else:
        result = Any

    if include_extras and node.metadata:
        result = _make_annotated(result, *node.metadata)

    return result

Cache management

Functions for managing the type inspection cache.

Functions:

Name Description
cache_info

Return type inspection cache statistics.

cache_clear

Clear the global type inspection cache.

typing_graph.cache_info

cache_info() -> _CacheInfo

Returns:

Type Description
_CacheInfo

A CacheInfo named tuple with:

_CacheInfo
  • hits: Number of cache hits
_CacheInfo
  • misses: Number of cache misses
_CacheInfo
  • maxsize: Maximum cache size (None means unbounded)
_CacheInfo
  • currsize: Current number of cached entries
Source code in src/typing_graph/_inspect_type.py
def cache_info() -> functools._CacheInfo:  # pyright: ignore[reportPrivateUsage]
    """Return type inspection cache statistics.

    Returns a named tuple with hits, misses, maxsize, and currsize.
    Useful for debugging and monitoring cache performance.

    Returns:
        A CacheInfo named tuple with:
        - hits: Number of cache hits
        - misses: Number of cache misses
        - maxsize: Maximum cache size (None means unbounded)
        - currsize: Current number of cached entries
    """
    return _inspect_type_cached.cache_info()

typing_graph.cache_clear

cache_clear() -> None
Source code in src/typing_graph/_inspect_type.py
def cache_clear() -> None:
    """Clear the global type inspection cache."""
    _inspect_type_cached.cache_clear()