Composable complex class support for attrs and dataclasses with (un)structuring and validation.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Powerful strategies for handling complex type scenarios including union types, subclass hierarchies, and class method integration. These utilities provide advanced configuration options for converters to handle sophisticated typing patterns and custom serialization requirements.
Strategies for handling union types with disambiguation and tagging support.
from cattrs.strategies import configure_tagged_union, configure_union_passthrough, default_tag_generator
from attrs import NOTHING
def configure_tagged_union(
union,
converter,
tag_generator=default_tag_generator,
tag_name="_type",
default=NOTHING
):
"""
Configure tagged union handling for converters.
Sets up a converter to handle union types by adding a discriminator tag
to distinguish between different union members during serialization.
Parameters:
- union: The union type to configure
- converter: The converter to configure
- tag_generator: Function to generate tags for union members (default: default_tag_generator)
- tag_name: Name of the discriminator field (default: "_type")
- default: Optional class to use if tag information is not present
"""
def configure_union_passthrough(union, converter, accept_ints_as_floats=True):
"""
Configure union passthrough for simple types.
Allows simple types in unions to pass through without modification,
useful for unions containing primitives or simple types that are natively
supported by serialization libraries.
Parameters:
- union: The union type to configure
- converter: The converter to configure
- accept_ints_as_floats: Whether to accept integers as valid floats
"""
def default_tag_generator(union_member_type):
"""
Default function for generating union tags.
Parameters:
- union_member_type: The type to generate a tag for
Returns:
String tag representing the type
"""from cattrs.strategies import configure_tagged_union
from cattrs import Converter
from attrs import define
from typing import Union
@define
class Dog:
name: str
breed: str
@define
class Cat:
name: str
lives: int
Animal = Union[Dog, Cat]
converter = Converter()
configure_tagged_union(Animal, converter)
# Tagged union serialization
dog = Dog("Buddy", "Golden Retriever")
dog_data = converter.unstructure(dog)
# Result: {'name': 'Buddy', 'breed': 'Golden Retriever', '_type': 'Dog'}
# Deserialization uses tag to determine type
dog_copy = converter.structure(dog_data, Animal)
# Returns: Dog instanceStrategies for automatically including and handling subclass hierarchies.
from cattrs.strategies import include_subclasses
def include_subclasses(
base_class,
converter,
**kwargs
):
"""
Configure converter to handle subclasses automatically.
Sets up the converter to recognize and handle all subclasses of a base class,
enabling polymorphic serialization and deserialization.
Parameters:
- base_class: The base class whose subclasses should be included
- converter: The converter to configure
- **kwargs: Additional configuration options
Returns:
The configured converter
"""from cattrs.strategies import include_subclasses
from cattrs import Converter
from attrs import define
@define
class Shape:
color: str
@define
class Circle(Shape):
radius: float
@define
class Rectangle(Shape):
width: float
height: float
converter = Converter()
include_subclasses(Shape, converter)
# Now all Shape subclasses are handled automatically
shapes = [
Circle(color="red", radius=5.0),
Rectangle(color="blue", width=10.0, height=20.0)
]
# Serialize list of mixed subclasses
shapes_data = converter.unstructure(shapes)
# Deserialize back to correct subclass types
shapes_copy = converter.structure(shapes_data, list[Shape])
# Returns: [Circle(...), Rectangle(...)]Strategies for using class-specific (un)structuring methods.
from cattrs.strategies import use_class_methods
def use_class_methods(
cl,
converter,
structure_method="from_dict",
unstructure_method="to_dict",
**kwargs
):
"""
Configure converter to use class-specific (un)structuring methods.
Allows classes to define their own serialization logic through class methods,
which the converter will use instead of the default structuring behavior.
Parameters:
- cl: The class to configure
- converter: The converter to configure
- structure_method: Name of the class method for structuring (default: "from_dict")
- unstructure_method: Name of the instance method for unstructuring (default: "to_dict")
- **kwargs: Additional configuration options
Returns:
The configured converter
"""from cattrs.strategies import use_class_methods
from cattrs import Converter
from attrs import define
from datetime import datetime
@define
class TimestampedData:
value: str
timestamp: datetime
@classmethod
def from_dict(cls, data):
# Custom deserialization logic
return cls(
value=data["value"],
timestamp=datetime.fromisoformat(data["timestamp"])
)
def to_dict(self):
# Custom serialization logic
return {
"value": self.value,
"timestamp": self.timestamp.isoformat()
}
converter = Converter()
use_class_methods(TimestampedData, converter)
# Now the converter uses the class's custom methods
data = TimestampedData("test", datetime.now())
serialized = converter.unstructure(data) # Uses to_dict()
deserialized = converter.structure(serialized, TimestampedData) # Uses from_dict()from cattrs import Converter
from typing import Set, List
def structure_set_from_list(val, _):
return set(val)
def unstructure_set_to_list(val):
return list(val)
converter = Converter()
converter.register_structure_hook(Set, structure_set_from_list)
converter.register_unstructure_hook(Set, unstructure_set_to_list)
# Sets are now serialized as lists
my_set = {1, 2, 3}
list_data = converter.unstructure(my_set) # [1, 2, 3]
set_copy = converter.structure(list_data, Set[int]) # {1, 2, 3}from cattrs.strategies import configure_tagged_union, include_subclasses
from cattrs import Converter
from typing import Union
@define
class Vehicle:
brand: str
@define
class Car(Vehicle):
doors: int
@define
class Motorcycle(Vehicle):
engine_size: float
VehicleUnion = Union[Car, Motorcycle]
converter = Converter()
# Combine strategies for comprehensive handling
include_subclasses(Vehicle, converter)
configure_tagged_union(VehicleUnion, converter)
# Now handles both subclass polymorphism and union discrimination
vehicles = [Car("Toyota", 4), Motorcycle("Harley", 1200.0)]
vehicle_data = converter.unstructure(vehicles)
vehicles_copy = converter.structure(vehicle_data, List[VehicleUnion])from cattrs.strategies import configure_tagged_union
from cattrs import Converter
def custom_tag_generator(cls):
"""Generate custom tags based on class name."""
return f"type_{cls.__name__.lower()}"
configure_tagged_union(
MyUnion,
converter,
tag_name="object_type",
tag_generator=custom_tag_generator
)
# Results in tags like: {"object_type": "type_dog", ...}Strategies work seamlessly with cattrs error handling, providing detailed validation errors when structuring fails:
from cattrs.strategies import configure_tagged_union
from cattrs import structure, ClassValidationError
try:
# Invalid tagged union data
bad_data = {"name": "Buddy", "_type": "InvalidType"}
animal = structure(bad_data, Animal)
except ClassValidationError as e:
print(f"Validation failed: {e}")
# Provides detailed error about unknown union tagStrategies integrate with cattrs' code generation system for optimal performance:
from cattrs.gen import make_dict_structure_fn
from cattrs.strategies import include_subclasses
# Subclass handling with optimized code generation
converter = Converter()
include_subclasses(Shape, converter)
# Generated functions are optimized for the configured strategies
structure_fn = make_dict_structure_fn(Shape, converter)from typing import TypeVar, Callable, Any, Union as TypingUnion
T = TypeVar('T')
# Type aliases for strategy functions
TagGenerator = Callable[[type], str]
UnionDiscriminator = Callable[[Any, type], bool]
StructureMethod = str
UnstructureMethod = strInstall with Tessl CLI
npx tessl i tessl/pypi-cattrs