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.
- Type introspection: Inspect any type annotation (generics,
Annotated,TypedDict, PEP 695 aliases) into structured nodes - Metadata hoisting: Extract
Annotatedmetadata and attach to base types - Metadata querying:
MetadataCollectionwithfind(),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 withTypeEdgeKind - 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.
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 |
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.
pip install typing-graph # or: uv add typing-graphRequires Python 3.10+. See Installation for package manager options and optional dependencies.
>>> 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.
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) # dictSee the functions tutorial for more details.
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) # TrueSee the structured types tutorial for dataclasses, TypedDict, NamedTuple, and more.
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 nodesUse 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, intUse 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.
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.
Full documentation is available at typing-graph.tbhb.dev.
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.
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.
MIT License. See LICENSE for details.