Inspecting structured types¶
In this tutorial, you'll inspect dataclasses, TypedDict, and NamedTuple to extract field definitions and their types. By the end, you'll be able to analyze any structured type and access its fields with their metadata.
Prerequisites
Before starting, ensure you have:
- Completed the Your first type inspection tutorial
- Basic familiarity with Python dataclasses
You don't need prior experience with TypedDict or NamedTuple.
Step 1: Create the script file¶
Create a new file called structured_types.py:
Run the script:
You should see:
Step 2: Define metadata constraint classes¶
Create metadata classes to attach to your dataclass fields:
from dataclasses import dataclass
@dataclass(frozen=True)
class MinLen:
value: int
@dataclass(frozen=True)
class Gt:
value: int | float
print("Constraint classes defined")
Run the script:
You should see:
Step 3: Define a dataclass with annotations¶
Create a dataclass with annotated fields:
from dataclasses import dataclass, field
from typing import Annotated
from typing_extensions import Doc
@dataclass(frozen=True)
class MinLen:
value: int
@dataclass(frozen=True)
class Gt:
value: int | float
@dataclass(frozen=True, slots=True)
class Order:
"""An order with validated fields."""
id: Annotated[str, MinLen(1), Doc("Unique order identifier")]
customer_email: Annotated[str, MinLen(5)]
total: Annotated[float, Gt(0), Doc("Order total in dollars")]
items: list[str] = field(default_factory=list)
notes: str | None = None
print(f"Order class defined with {len(Order.__dataclass_fields__)} fields")
Run the script:
You should see:
Step 4: Inspect the dataclass¶
Use inspect_dataclass() to analyze your dataclass:
from dataclasses import dataclass, field
from typing import Annotated
from typing_extensions import Doc
from typing_graph import inspect_dataclass
@dataclass(frozen=True)
class MinLen:
value: int
@dataclass(frozen=True)
class Gt:
value: int | float
@dataclass(frozen=True, slots=True)
class Order:
"""An order with validated fields."""
id: Annotated[str, MinLen(1), Doc("Unique order identifier")]
customer_email: Annotated[str, MinLen(5)]
total: Annotated[float, Gt(0), Doc("Order total in dollars")]
items: list[str] = field(default_factory=list)
notes: str | None = None
node = inspect_dataclass(Order)
print(f"Node type: {type(node).__name__}")
Run the script:
You should see:
Step 5: Access dataclass properties¶
The DataclassNode provides access to dataclass-specific information:
from dataclasses import dataclass, field
from typing import Annotated
from typing_extensions import Doc
from typing_graph import inspect_dataclass
@dataclass(frozen=True)
class MinLen:
value: int
@dataclass(frozen=True)
class Gt:
value: int | float
@dataclass(frozen=True, slots=True)
class Order:
"""An order with validated fields."""
id: Annotated[str, MinLen(1), Doc("Unique order identifier")]
customer_email: Annotated[str, MinLen(5)]
total: Annotated[float, Gt(0), Doc("Order total in dollars")]
items: list[str] = field(default_factory=list)
notes: str | None = None
node = inspect_dataclass(Order)
print(f"Class: {node.cls.__name__}")
print(f"Frozen: {node.frozen}")
print(f"Slots: {node.slots}")
print(f"Number of fields: {len(node.fields)}")
Run the script:
You should see:
Checkpoint
At this point, you have:
- Defined a dataclass with annotated fields
- Inspected it using
inspect_dataclass() - Accessed class-level properties like
frozenandslots
Step 6: Iterate over field definitions¶
The fields attribute returns a tuple of DataclassFieldDef objects. Each field definition provides access to the field's name, type, default values, and whether it's required:
from dataclasses import dataclass, field
from typing import Annotated
from typing_extensions import Doc
from typing_graph import inspect_dataclass
@dataclass(frozen=True)
class MinLen:
value: int
@dataclass(frozen=True)
class Gt:
value: int | float
@dataclass(frozen=True, slots=True)
class Order:
"""An order with validated fields."""
id: Annotated[str, MinLen(1), Doc("Unique order identifier")]
customer_email: Annotated[str, MinLen(5)]
total: Annotated[float, Gt(0), Doc("Order total in dollars")]
items: list[str] = field(default_factory=list)
notes: str | None = None
node = inspect_dataclass(Order)
for field_def in node.fields:
has_default = field_def.default is not None or field_def.default_factory is not None
print(f"{field_def.name}: required={field_def.required}, has_default={has_default}")
Run the script:
You should see:
id: required=True, has_default=False
customer_email: required=True, has_default=False
total: required=True, has_default=False
items: required=False, has_default=True
notes: required=False, has_default=True
Step 7: Access field type nodes and metadata¶
Each field definition has a type attribute containing the inspected type node:
from dataclasses import dataclass, field
from typing import Annotated
from typing_extensions import Doc
from typing_graph import inspect_dataclass
@dataclass(frozen=True)
class MinLen:
value: int
@dataclass(frozen=True)
class Gt:
value: int | float
@dataclass(frozen=True, slots=True)
class Order:
"""An order with validated fields."""
id: Annotated[str, MinLen(1), Doc("Unique order identifier")]
customer_email: Annotated[str, MinLen(5)]
total: Annotated[float, Gt(0), Doc("Order total in dollars")]
items: list[str] = field(default_factory=list)
notes: str | None = None
node = inspect_dataclass(Order)
for field_def in node.fields:
type_node = field_def.type
print(f"{field_def.name}: {type(type_node).__name__}")
if type_node.metadata:
print(f" Metadata: {list(type_node.metadata)}")
Run the script:
You should see:
id: ConcreteNode
Metadata: [MinLen(value=1), Doc(documentation='Unique order identifier')]
customer_email: ConcreteNode
Metadata: [MinLen(value=5)]
total: ConcreteNode
Metadata: [Gt(value=0), Doc(documentation='Order total in dollars')]
items: SubscriptedGenericNode
notes: UnionNode
Step 8: Traverse into complex field types¶
Use the node's properties to explore complex field types:
from dataclasses import dataclass, field
from typing import Annotated
from typing_extensions import Doc
from typing_graph import inspect_dataclass, ConcreteNode
@dataclass(frozen=True)
class MinLen:
value: int
@dataclass(frozen=True)
class Gt:
value: int | float
@dataclass(frozen=True, slots=True)
class Order:
"""An order with validated fields."""
id: Annotated[str, MinLen(1), Doc("Unique order identifier")]
customer_email: Annotated[str, MinLen(5)]
total: Annotated[float, Gt(0), Doc("Order total in dollars")]
items: list[str] = field(default_factory=list)
notes: str | None = None
node = inspect_dataclass(Order)
# Get the 'items' field (list[str])
items_field = next(f for f in node.fields if f.name == "items")
items_type = items_field.type
print(f"Items origin: {items_type.origin.cls}")
print(f"Items element type: {items_type.args[0].cls}")
# Get the 'notes' field (str | None)
notes_field = next(f for f in node.fields if f.name == "notes")
notes_type = notes_field.type
print(f"\nNotes union members: {len(notes_type.members)}")
for member in notes_type.members:
if isinstance(member, ConcreteNode):
print(f" - {member.cls}")
Run the script:
You should see:
Items origin: <class 'list'>
Items element type: <class 'str'>
Notes union members: 2
- <class 'str'>
- <class 'NoneType'>
Checkpoint
At this point, you have:
- Iterated over field definitions
- Accessed field type nodes and their metadata
- Traversed into complex field types like
list[str]andstr | None
Step 9: Define a TypedDict¶
Create a TypedDict class to inspect:
from typing import TypedDict
from typing_extensions import NotRequired, Required
class UserProfile(TypedDict, total=False):
username: Required[str]
email: Required[str]
bio: NotRequired[str]
age: int
print(f"UserProfile defined with total={UserProfile.__total__}")
Run the script:
You should see:
Step 10: Inspect the TypedDict¶
Use inspect_typed_dict() to analyze it:
from typing import TypedDict
from typing_extensions import NotRequired, Required
from typing_graph import inspect_typed_dict
class UserProfile(TypedDict, total=False):
username: Required[str]
email: Required[str]
bio: NotRequired[str]
age: int
node = inspect_typed_dict(UserProfile)
print(f"Node type: {type(node).__name__}")
print(f"Total: {node.total}")
Run the script:
You should see:
Step 11: Access TypedDict field requirements¶
The TypedDictNode provides field information with required status:
from typing import TypedDict
from typing_extensions import NotRequired, Required
from typing_graph import inspect_typed_dict
class UserProfile(TypedDict, total=False):
username: Required[str]
email: Required[str]
bio: NotRequired[str]
age: int
node = inspect_typed_dict(UserProfile)
for field_def in node.fields:
print(f"{field_def.name}: required={field_def.required}")
Run the script:
You should see:
Checkpoint
At this point, you have:
- Defined a TypedDict with
RequiredandNotRequiredfields - Inspected it using
inspect_typed_dict() - Accessed field requirement status
Step 12: Use inspect_class() for auto-detection¶
When you don't know the specific class kind, use inspect_class():
from dataclasses import dataclass
from typing import TypedDict
from typing_graph import inspect_class, DataclassNode, TypedDictNode
@dataclass
class Point:
x: float
y: float
class Config(TypedDict):
name: str
value: int
# Works with dataclasses
point_node = inspect_class(Point)
print(f"Point: {type(point_node).__name__}, is DataclassNode: {isinstance(point_node, DataclassNode)}")
# Works with TypedDict
config_node = inspect_class(Config)
print(f"Config: {type(config_node).__name__}, is TypedDictNode: {isinstance(config_node, TypedDictNode)}")
Run the script:
You should see:
Step 13: Define and inspect a NamedTuple¶
NamedTuple classes work similarly with inspect_named_tuple(). The resulting NamedTupleNode provides field definitions with name, type, and required status:
from typing import NamedTuple
from typing_graph import inspect_named_tuple
class Point(NamedTuple):
x: float
y: float
label: str = "origin"
node = inspect_named_tuple(Point)
print(f"Node type: {type(node).__name__}")
for field_def in node.fields:
print(f"{field_def.name}: {field_def.type.cls.__name__}, required={field_def.required}")
Run the script:
You should see:
Node type: NamedTupleNode
x: float, required=True
y: float, required=True
label: str, required=False
Checkpoint
You've completed this tutorial. You can now:
- Inspect dataclasses with
inspect_dataclass() - Inspect TypedDict with
inspect_typed_dict() - Inspect NamedTuple with
inspect_named_tuple() - Use
inspect_class()for auto-detection - Access field definitions, types, and metadata
Summary¶
You've learned how to inspect structured types and extract their field definitions. The key functions are:
inspect_dataclass()for dataclassesinspect_typed_dict()for TypedDictinspect_named_tuple()for NamedTupleinspect_class()for auto-detection
Each returns a node with a fields attribute containing field definitions with name, type, and requirement status.
Next steps
Now that you can inspect structured types, explore:
- Inspecting functions - Analyze function signatures
- Metadata queries - Patterns for processing field metadata
- Walking the type graph - Traverse complex type structures
For background on why structured types are handled this way, see the Architecture overview.