Skip to content

tbhb/typing-graph

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

typing-graph

CI PyPI version codecov Python 3.10+ License: MIT

A building block for Python libraries that derive runtime behavior from type annotations. Pass any type (generics, Annotated, dataclasses, TypedDict, PEP 695 aliases) and get back a graph of nodes with metadata hoisting, qualifier detection, and semantic edge information.

Warning

Early development. APIs may change. Not yet recommended for production use.

Built on Pydantic's typing-inspection and designed for compatibility with annotated-types.

Features

  • Type introspection: Inspect any type annotation (generics, Annotated, TypedDict, PEP 695 aliases) into structured nodes
  • Metadata hoisting: Extract Annotated metadata and attach to base types
  • Metadata querying: MetadataCollection with find(), filter(), get(), and protocol matching
  • Qualifier extraction: Detect ClassVar, Final, Required, NotRequired, ReadOnly, InitVar
  • Graph traversal: walk() for depth-first iteration, edges() for semantic relationships with TypeEdgeKind
  • Structured types: dataclass, TypedDict, NamedTuple, Protocol, Enum
  • Modern Python: PEP 695 type parameters, PEP 696 defaults, TypeGuard, TypeIs

Planned: annotated-types integration, visitor pattern, attrs/Pydantic support. See the roadmap.

Use cases

typing-graph provides the foundation for frameworks that derive behavior from type annotations:

Use case Description
Validation frameworks Extract constraints from Annotated metadata and generate validation logic based on type structure
Type conversion Convert values between types by inspecting source and target type structures, handling nested generics and union types
Command-line interfaces Parse command-line arguments by inspecting function signatures and generating appropriate parsers for each parameter type
ORM mapping Map Python classes to database schemas by analyzing field types, extracting column metadata from annotations
Feature flags Extract feature flag definitions from type metadata to configure runtime behavior based on annotated types
Code generation Generate serializers, API clients, or documentation by traversing the type graph and emitting code for each node type

What typing-graph is not

typing-graph is not a runtime type checker or validation library. It provides the introspection layer that such tools can build on.

If you want to Use instead
Check types at runtime beartype, typeguard
Validate data pydantic, attrs
Static type checking basedpyright, mypy, pyrefly, ty

typing-graph helps you build validation frameworks by inspecting type structures; it doesn't validate data itself.

Installation

pip install typing-graph  # or: uv add typing-graph

Requires Python 3.10+. See Installation for package manager options and optional dependencies.

Quick start

>>> from typing import Annotated
>>> from dataclasses import dataclass
>>> from typing_graph import inspect_type

>>> # Define constraint metadata (like you might in a validation framework)
>>> @dataclass
... class Pattern:
...     regex: str

>>> @dataclass
... class MinLen:
...     value: int

>>> # Define a reusable annotated type alias
>>> URL = Annotated[str, Pattern(r"^https?://")]

>>> # Build a complex nested type
>>> Urls = Annotated[list[URL], MinLen(1)]

>>> # Inspect the type graph
>>> node = inspect_type(Urls)

>>> # The outer node is a SubscriptedGenericNode (list) with container-level metadata
>>> node.origin.cls
<class 'list'>
>>> node.metadata.find(MinLen)
MinLen(value=1)

>>> # Traverse to the element type - it carries its own metadata
>>> element = node.args[0]
>>> element.cls
<class 'str'>
>>> element.metadata.find(Pattern)
Pattern(regex='^https?://')

Each node in the graph carries its own metadata, enabling frameworks to apply different validation or transformation logic at each level of the type structure. See the first inspection tutorial for a complete walkthrough.

Inspecting functions

from typing import Annotated
from typing_graph import inspect_function

def fetch_users(
    limit: Annotated[int, "max results"] = 10,
    tags: list[str] | None = None,
) -> list[dict[str, str]]:
    ...

func = inspect_function(fetch_users)
print(func.name)  # "fetch_users"

# Parameters carry their type nodes and metadata
limit_param = func.signature.parameters[0]
print(limit_param.name)  # "limit"
print(limit_param.metadata.get(str))  # "max results"

# Return type is fully inspected
returns = func.signature.returns
print(returns.origin.cls)  # list
print(returns.args[0].origin.cls)  # dict

See the functions tutorial for more details.

Inspecting classes

from dataclasses import dataclass, field
from typing import Annotated
from typing_graph import inspect_class, DataclassNode

@dataclass(frozen=True, slots=True)
class User:
    name: str
    email: Annotated[str, "unique"]
    roles: list[str] = field(default_factory=list)

node = inspect_class(User)
assert isinstance(node, DataclassNode)
assert node.frozen is True
assert node.slots is True

# Fields preserve type structure and metadata
email_field = node.fields[1]
print(email_field.name)  # "email"
print(email_field.metadata.get(str))  # "unique"

roles_field = node.fields[2]
print(roles_field.type.origin.cls)  # list
print(roles_field.default_factory)  # True

See the structured types tutorial for dataclasses, TypedDict, NamedTuple, and more.

Inspecting modules

from typing_graph import inspect_module
import mymodule

types = inspect_module(mymodule)
print(types.classes)      # Dict of class names to inspection results
print(types.functions)    # Dict of function names to FunctionNodes
print(types.type_aliases) # Dict of type alias names to alias nodes

Graph traversal

Use walk() for depth-first traversal with optional filtering and depth limits:

from typing_graph import inspect_type, is_concrete_node, walk, ConcreteNode
from typing_extensions import TypeIs

node = inspect_type(dict[str, list[int]])

# Iterate all nodes
for n in walk(node):
    print(n)

for n in walk(node, predicate=is_concrete_node):
    print(n.cls)  # str, int

Use edges() on any node for semantic relationship information:

for conn in node.edges():
    print(conn.edge.kind, conn.target)
# TypeEdgeKind.ORIGIN SubscriptedGenericNode(...)
# TypeEdgeKind.TYPE_ARG ConcreteNode(cls=str)
# TypeEdgeKind.TYPE_ARG SubscriptedGenericNode(...)

TypeEdgeKind describes relationships: ORIGIN, TYPE_ARG, ELEMENT, FIELD, PARAM, RETURN, UNION_MEMBER, and more. See the traversing type graphs tutorial and graph edges explanation for details.

Configuration

from typing_graph import inspect_type, InspectConfig, EvalMode

config = InspectConfig(
    eval_mode=EvalMode.DEFERRED,  # EAGER, DEFERRED (default), or STRINGIFIED
    max_depth=50,
    hoist_metadata=True,
)
node = inspect_type(SomeType, config=config)

See the configuration guide for all available options.

Documentation

Full documentation is available at typing-graph.tbhb.dev.

Acknowledgments

Pydantic's approach to type introspection and metadata extraction inspired this library. typing-graph builds on Pydantic's typing-inspection library for low-level type introspection.

AI help

This project uses Claude Code as a development tool for:

  • Rubber ducking and exploring architecture and design alternatives
  • Drafting documentation and docstrings
  • Generating test scaffolding and boilerplate
  • Code cleanup and refactoring suggestions
  • Researching Python typing edge cases
  • Running benchmarks and mutation testing
  • Release automation

All contributions undergo review and testing before inclusion, regardless of origin.

License

MIT License. See LICENSE for details.

About

Extract metadata from Python type annotations into lazy, cached graphs

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Contributors 3

  •  
  •  
  •