Skip to content

Your first type inspection

In this tutorial, you'll inspect Python type annotations and extract metadata from them using inspect_type(). By the end, you'll have a working script that traverses a type graph and prints its structure.

Prerequisites

Before starting, ensure you have:

  • Python 3.10 or later installed
  • A terminal or command prompt
  • Basic familiarity with Python type hints

You don't need prior experience with typing-graph.

Step 1: Install typing-graph

Install the package using pip:

Terminal
pip install typing-graph

You should see output indicating a successful installation:

Output
Successfully installed typing-graph-x.x.x

Step 2: Create the script file

Create a new file called inspect_types.py:

inspect_types.py
from typing_graph import inspect_type, ConcreteNode

Step 3: Inspect a simple type

Add code to inspect the int type:

inspect_types.py
from typing_graph import inspect_type, ConcreteNode

# Inspect a simple type
node = inspect_type(int)
print(f"Node type: {type(node).__name__}")
print(f"Class: {node.cls}")

Run the script:

Terminal
python inspect_types.py

You should see:

Output
Node type: ConcreteNode
Class: <class 'int'>

Step 4: Verify the node type and class

Simple types like int, str, and custom classes return a ConcreteNode. All type nodes inherit from TypeNode. The cls attribute gives you the underlying Python class.

Update your script to also inspect a custom class:

inspect_types.py
from typing_graph import inspect_type, ConcreteNode

# Inspect a simple type
node = inspect_type(int)
print(f"Node type: {type(node).__name__}")
print(f"Class: {node.cls}")

# Inspect a custom class
class User:
    pass

user_node = inspect_type(User)
print(f"\nUser node type: {type(user_node).__name__}")
print(f"User class: {user_node.cls}")

Run the script:

Terminal
python inspect_types.py

You should see:

Output
Node type: ConcreteNode
Class: <class 'int'>

User node type: ConcreteNode
User class: <class '__main__.User'>

Checkpoint

At this point, you have:

  • Installed typing-graph
  • Inspected simple types using inspect_type()
  • Accessed the underlying class via the cls attribute

Step 5: Inspect a generic type

Replace your script contents with code to inspect a generic type:

inspect_types.py
from typing_graph import inspect_type, SubscriptedGenericNode

# Inspect a generic type
node = inspect_type(list[int])
print(f"Node type: {type(node).__name__}")

Run the script:

Terminal
python inspect_types.py

You should see:

Output
Node type: SubscriptedGenericNode

Step 6: Access generic origin and arguments

Generic types like list[int] return a SubscriptedGenericNode. This node provides access to both the origin type and its type arguments.

Update your script:

inspect_types.py
from typing_graph import inspect_type, SubscriptedGenericNode

# Inspect a generic type
node = inspect_type(list[int])
print(f"Node type: {type(node).__name__}")

# Access the origin (the generic type itself)
print(f"Origin class: {node.origin.cls}")

# Access the type arguments
print(f"Number of args: {len(node.args)}")
print(f"First arg class: {node.args[0].cls}")

Run the script:

Terminal
python inspect_types.py

You should see:

Output
Node type: SubscriptedGenericNode
Origin class: <class 'list'>
Number of args: 1
First arg class: <class 'int'>

Checkpoint

At this point, you have:

  • Inspected generic types like list[int]
  • Accessed the origin class via node.origin.cls
  • Accessed type arguments via node.args

Step 7: Define metadata classes

Now create metadata classes that you'll attach to types. Replace your script:

inspect_types.py
from dataclasses import dataclass

# Define some metadata classes
@dataclass(frozen=True)
class MinLen:
    value: int

@dataclass(frozen=True)
class MaxLen:
    value: int

print("Metadata classes defined")

Run the script:

Terminal
python inspect_types.py

You should see:

Output
Metadata classes defined

Step 8: Create an annotated type

Add an annotated type using your metadata classes:

inspect_types.py
from dataclasses import dataclass
from typing import Annotated

# Define some metadata classes
@dataclass(frozen=True)
class MinLen:
    value: int

@dataclass(frozen=True)
class MaxLen:
    value: int

# Create an annotated type
ValidatedString = Annotated[str, MinLen(1), MaxLen(100)]
print(f"Type alias created: {ValidatedString}")

Run the script:

Terminal
python inspect_types.py

You should see:

Output
Type alias created: typing.Annotated[str, MinLen(value=1), MaxLen(value=100)]

Step 9: Inspect the annotated type

Add inspection of the annotated type:

inspect_types.py
from dataclasses import dataclass
from typing import Annotated

from typing_graph import inspect_type

# Define some metadata classes
@dataclass(frozen=True)
class MinLen:
    value: int

@dataclass(frozen=True)
class MaxLen:
    value: int

# Create an annotated type
ValidatedString = Annotated[str, MinLen(1), MaxLen(100)]

# Inspect it
node = inspect_type(ValidatedString)
print(f"Node type: {type(node).__name__}")
print(f"Class: {node.cls}")

Run the script:

Terminal
python inspect_types.py

You should see:

Output
Node type: ConcreteNode
Class: <class 'str'>

Notice that you get a ConcreteNode for str, not an AnnotatedNode. typing-graph "hoists" metadata from Annotated wrappers to the base type node. See metadata hoisting for more details.

Step 10: Access metadata from the node

Add code to access the metadata:

inspect_types.py
from dataclasses import dataclass
from typing import Annotated

from typing_graph import inspect_type

# Define some metadata classes
@dataclass(frozen=True)
class MinLen:
    value: int

@dataclass(frozen=True)
class MaxLen:
    value: int

# Create an annotated type
ValidatedString = Annotated[str, MinLen(1), MaxLen(100)]

# Inspect it
node = inspect_type(ValidatedString)
print(f"Node type: {type(node).__name__}")
print(f"Class: {node.cls}")

# Access metadata
print(f"\nMetadata: {node.metadata}")
for meta in node.metadata:
    if isinstance(meta, MinLen):
        print(f"  Minimum length: {meta.value}")
    elif isinstance(meta, MaxLen):
        print(f"  Maximum length: {meta.value}")

Run the script:

Terminal
python inspect_types.py

You should see:

Output
Node type: ConcreteNode
Class: <class 'str'>

Metadata: MetadataCollection([MinLen(value=1), MaxLen(value=100)])
  Minimum length: 1
  Maximum length: 100

Checkpoint

At this point, you have:

  • Created metadata classes using frozen dataclasses
  • Attached metadata to types using Annotated
  • Extracted metadata from inspected type nodes

Step 11: Define a nested annotated type

Replace your script with code that creates nested annotated types:

inspect_types.py
from dataclasses import dataclass
from typing import Annotated

from typing_graph import inspect_type

@dataclass(frozen=True)
class Description:
    text: str

# Metadata on the element type (via a type alias)
URL = Annotated[str, Description("A URL string")]

# Metadata on the list itself
URLs = Annotated[list[URL], Description("A list of URLs")]

# Inspect the outer type
node = inspect_type(URLs)

# The list node has its own metadata
print(f"List metadata: {list(node.metadata)}")

# The element type has its metadata
element = node.args[0]
print(f"Element metadata: {list(element.metadata)}")

Run the script:

Terminal
python inspect_types.py

You should see:

Output
List metadata: [Description(text='A list of URLs')]
Element metadata: [Description(text='A URL string')]

Step 12: Traverse the type tree

Replace your script with a recursive traversal function:

inspect_types.py
from typing_graph import inspect_type, ConcreteNode, SubscriptedGenericNode


def print_type_tree(node, indent=0):
    """Recursively print a type tree."""
    prefix = "  " * indent
    node_name = type(node).__name__

    # Add details based on node type
    if isinstance(node, ConcreteNode):
        print(f"{prefix}{node_name}: {node.cls.__name__}")
    elif isinstance(node, SubscriptedGenericNode):
        print(f"{prefix}{node_name}: {node.origin.cls.__name__}[...]")
    else:
        print(f"{prefix}{node_name}")

    # Recurse into children
    for child in node.children():
        print_type_tree(child, indent + 1)


# Try it out
node = inspect_type(dict[str, list[int]])
print_type_tree(node)

Run the script:

Terminal
python inspect_types.py

You should see:

Output
SubscriptedGenericNode: dict[...]
  ConcreteNode: str
  SubscriptedGenericNode: list[...]
    ConcreteNode: int

Checkpoint

You've completed this tutorial. You can now:

  • Inspect any Python type annotation
  • Access the underlying class and type arguments
  • Extract metadata from Annotated types
  • Traverse nested type structures using children()

Prefer walk() for traversal

The children() method gives you direct access to child nodes, but for most traversal tasks, the walk() iterator is simpler. It handles depth-first traversal automatically and supports filtering with predicates. See How to filter type graphs with walk() for practical examples.

Summary

You've built a script that inspects Python type annotations and traverses the type graph. The key functions are:

  • inspect_type() inspects any type annotation
  • node.cls accesses the underlying class
  • node.metadata accesses attached metadata
  • node.children() returns child nodes for traversal

Next steps

Now that you understand the basics, explore:

For practical applications of type graph traversal, see Walking the type graph.

For helper functions like is_optional_node(), unwrap_optional(), and get_union_members(), see Common helper functions.