Recursive
coola.recursive ¶
Utilities to recursively transform nested data structures.
Properties:
- Leaves only:
funcis called on the leaves, once per occurrence, never on a container. - Structure-preserving: a container is rebuilt from the transformed children with its original type. Keys are never transformed.
- Type dispatch: the transformer is chosen from the type of the data, along the Method Resolution Order.
- Registry-extensible: a new type is supported by registering a transformer.
- Deterministic order: the leaves are visited depth first, left to right.
- Pure: the input is never modified.
- Any depth: no
RecursionError, the traversal uses an explicit stack. - Cycle-safe: a container that contains itself raises a
ValueError. - Errors propagate: an exception raised by
funcpropagates unchanged.
Not guaranteed: laziness (the output is built whole), the cost of a container shared by several paths (it is transformed once per path), and the order of the calls inside a set.
Example
>>> from coola.recursive import recursive_apply
>>> recursive_apply({"a": 1, "b": 2}, lambda x: x * 10)
{'a': 10, 'b': 20}
coola.recursive.BaseTransformer ¶
Bases: ABC, Generic[T]
Base class for type-specific transformers.
A transformer only describes the shape of a type: its children and
how to rebuild it from the transformed children. The engine
(coola.recursive.engine) walks the children, applies the function
to the leaves and detects the cycles, so a transformer cannot forget
a protection and does not recurse.
The contract of expand:
- It is pure and cheap: it does not call the function, does not call
the registry, does not recurse and does not modify
data. rebuildreceives a newlistwith the transformed children, in the order ofchildren, and returns a new container (it returnsdataitself only if it has no children and must be kept as it is, likeIdentityTransformer).- A container with no child (
[],{}) returns emptychildren:rebuild([])builds an empty container of the same type. - A leaf returns
None: the engine calls the function on it.
Notes
Subclasses must implement the expand method.
Example
>>> from coola.recursive import SequenceTransformer
>>> transformer = SequenceTransformer()
>>> transformer
SequenceTransformer()
>>> children, rebuild = transformer.expand([1, 2, 3])
>>> children
[1, 2, 3]
>>> rebuild(["1", "2", "3"])
['1', '2', '3']
coola.recursive.BaseTransformer.expand
abstractmethod
¶
expand(data: T) -> Expansion | None
Describe the children of data and how to rebuild it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
T
|
The data to expand. Must be of type T that this transformer handles. |
required |
Returns:
| Type | Description |
|---|---|
Expansion | None
|
|
Expansion | None
|
|
Expansion | None
|
function that rebuilds |
Expansion | None
|
transformed children. |
Example
>>> from coola.recursive import DefaultTransformer, MappingTransformer
>>> DefaultTransformer().expand(42) is None
True
>>> children, rebuild = MappingTransformer().expand({"a": 1, "b": 2})
>>> children
[1, 2]
>>> rebuild([10, 20])
{'a': 10, 'b': 20}
coola.recursive.DataclassTransformer ¶
Bases: InlineDisplayMixin, BaseTransformer[Any]
Transformer for dataclass instances that recursively transforms the fields.
The fields are the children. The instance is rebuilt with a shallow
copy in which every field is set with object.__setattr__, so a
frozen or slotted dataclass is supported, the fields with
init=False are transformed, an InitVar without a default
value is not a problem, the input is not modified, and no user code
(__init__, __post_init__) runs during the traversal.
Notes
__post_init__is not run again, so a field derived from another field in__post_init__is not recomputed: it can become inconsistent with the field it was derived from after the function is applied. Register a transformer for the type to rebuild it differently.- It is registered as a fallback for
dataclasses.is_dataclassin the default registry. A transformer registered for the type of the dataclass has priority; registerDefaultTransformerto keep a dataclass as a leaf.
Example
>>> from dataclasses import dataclass
>>> from coola.recursive import (
... DataclassTransformer,
... DefaultTransformer,
... SequenceTransformer,
... TransformerRegistry,
... )
>>> @dataclass
... class Config:
... lr: float
... layers: list
...
>>> transformer = DataclassTransformer()
>>> registry = TransformerRegistry(
... {object: DefaultTransformer(), list: SequenceTransformer(), Config: transformer}
... )
>>> registry.transform(Config(0.1, [1, 2]), str)
Config(lr='0.1', layers=['1', '2'])
coola.recursive.DefaultTransformer ¶
Bases: InlineDisplayMixin, BaseTransformer[Any]
Transformer for leaf nodes: the engine applies the function to the data.
This is the default transformer used for values that don't require recursive traversal. It returns no children, so the data is a leaf. This transformer is typically used as the terminal case in recursive transformations when a leaf value (non-container) is encountered.
Notes
Unlike container-specific transformers (e.g., list, dict), this transformer does not traverse nested structures. It treats all input as atomic values, even a container.
Example
>>> from coola.recursive import DefaultTransformer, TransformerRegistry
>>> transformer = DefaultTransformer()
>>> transformer
DefaultTransformer()
>>> # Nothing to expand: the data is a leaf
>>> transformer.expand(42) is None
True
>>> # Even container types are treated as atomic values
>>> registry = TransformerRegistry({list: transformer})
>>> registry.transform([1, 2, 3], str)
'[1, 2, 3]'
coola.recursive.IdentityTransformer ¶
Bases: BaseTransformer[Any]
Transformer for values that are kept as they are: the function is never applied.
This transformer returns no children and rebuilds the value as is, so the engine does not call the function. It is useful to register for types that must be preserved as-is while traversing a nested data structure (e.g. to skip certain leaf types or a whole subtree instead of transforming them).
Example
>>> from coola.recursive import IdentityTransformer, TransformerRegistry
>>> transformer = IdentityTransformer()
>>> registry = TransformerRegistry({object: transformer})
>>> registry.transform([1, 2], str)
[1, 2]
coola.recursive.IdentityTransformer.expand ¶
expand(data: Any) -> Expansion | None
Describe data as a value without children that is
rebuilt as is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Any
|
The data to keep unchanged. |
required |
Returns:
| Type | Description |
|---|---|
Expansion | None
|
No children, and a function that returns |
Example
>>> from coola.recursive import IdentityTransformer
>>> children, rebuild = IdentityTransformer().expand(42)
>>> children
()
>>> rebuild([])
42
coola.recursive.MappingTransformer ¶
Bases: InlineDisplayMixin, BaseTransformer[Mapping[Any, Any]]
Transformer for mapping types that recursively transforms values.
This transformer handles dict-like mapping structures by recursively transforming all values while preserving keys unchanged. After transformation, it reconstructs the mapping using its original type (dict, OrderedDict, defaultdict, etc.), maintaining the mapping's specific characteristics and behavior.
Notes
- Keys are never transformed, only values are processed recursively
- The original mapping type is preserved in the output
- Nested mappings and other containers in values are handled recursively
- Empty mappings are preserved as empty mappings of the same type
Example
>>> from coola.recursive import MappingTransformer, TransformerRegistry, DefaultTransformer
>>> transformer = MappingTransformer()
>>> transformer
MappingTransformer()
>>> children, rebuild = transformer.expand({"a": 1, "b": 2})
>>> children
[1, 2]
>>> rebuild([10, 20])
{'a': 10, 'b': 20}
>>> registry = TransformerRegistry({object: DefaultTransformer(), dict: transformer})
>>> # Transform simple dict values
>>> registry.transform({"a": 1, "b": 2}, str)
{'a': '1', 'b': '2'}
>>> # Keys remain unchanged
>>> registry.transform({1: "x", 2: "y"}, str.upper)
{1: 'X', 2: 'Y'}
>>> # Empty mappings are preserved
>>> registry.transform({}, str)
{}
coola.recursive.SequenceTransformer ¶
Bases: InlineDisplayMixin, BaseTransformer[Sequence[Any]]
Transformer for sequence types that recursively transforms elements.
This transformer handles sequence structures (list, tuple, namedtuple, etc.) by recursively transforming all elements while preserving the original sequence type and structure. After transformation, it reconstructs the sequence using its original type, with special handling for named tuples to preserve their field structure.
Notes
- All elements are transformed recursively by the engine
- The original sequence type is preserved (list remains list, tuple remains tuple)
- Named tuples receive special handling to preserve field names
- Nested sequences and other containers are handled recursively
- Empty sequences are preserved as empty sequences of the same type
- String sequences (str) should typically use a different transformer as they are often treated as atomic values
Example
>>> from coola.recursive import SequenceTransformer, TransformerRegistry, DefaultTransformer
>>> transformer = SequenceTransformer()
>>> transformer
SequenceTransformer()
>>> children, rebuild = transformer.expand((1, 2, 3))
>>> children
[1, 2, 3]
>>> rebuild(["1", "2", "3"])
('1', '2', '3')
>>> registry = TransformerRegistry({object: DefaultTransformer(), list: transformer})
>>> # Transform list elements
>>> registry.transform([1, 2, 3], str)
['1', '2', '3']
>>> # Nested sequences are handled recursively
>>> registry.transform([[1, 2], [3, 4]], str)
[['1', '2'], ['3', '4']]
>>> # Empty sequences are preserved
>>> registry.transform([], str)
[]
>>> # Named tuples preserve their structure
>>> from collections import namedtuple
>>> Point = namedtuple("Point", ["x", "y"])
>>> TransformerRegistry({object: DefaultTransformer(), Point: transformer}).transform(
... Point(1, 2), str
... )
Point(x='1', y='2')
coola.recursive.SetTransformer ¶
Bases: InlineDisplayMixin, BaseTransformer[Set[Any]]
Transformer for set types that recursively transforms elements.
This transformer handles set structures (set, frozenset) by recursively transforming all elements while preserving the original set type. After transformation, it reconstructs the set using its original type. Sets maintain their unordered, unique-element properties.
Important
HASHABILITY REQUIREMENT: All transformed values MUST remain hashable (i.e., immutable and hashable) since sets can only contain hashable elements. Transforming to unhashable types (like lists or dicts) will raise a TypeError.
Notes
- All elements are transformed recursively by the engine
- The original set type is preserved (set remains set, frozenset remains frozenset)
- Element order is not guaranteed (sets are unordered)
- Duplicate transformed values will be automatically deduplicated
- Empty sets are preserved as empty sets of the same type
- If transformation produces unhashable values, a TypeError will be raised
Example
>>> from coola.recursive import SetTransformer, TransformerRegistry, DefaultTransformer
>>> transformer = SetTransformer()
>>> transformer
SetTransformer()
>>> registry = TransformerRegistry({object: DefaultTransformer(), set: transformer})
>>> # Transform set elements (order may vary in output)
>>> registry.transform({1}, str)
{'1'}
>>> # Duplicate values after transformation are automatically deduplicated
>>> registry.transform({1, 2, 3}, lambda x: x // 4)
{0}
coola.recursive.SetTransformer.expand ¶
expand(data: Set[Any]) -> Expansion | None
Expand an object into its children.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Set[Any]
|
The object. |
required |
Returns:
| Type | Description |
|---|---|
Expansion | None
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
From the function that rebuilds the set, if a transformed value is not hashable. |
coola.recursive.Transformer ¶
Bases: Protocol[T_contra]
Structural interface of a type-specific transformer.
Any object with a compatible expand method can be registered
without subclassing BaseTransformer, which remains available as
a convenience base class.
coola.recursive.Transformer.expand ¶
expand(data: T_contra) -> Expansion | None
Describe the children of data and how to rebuild it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
T_contra
|
The data to expand. |
required |
Returns:
| Type | Description |
|---|---|
Expansion | None
|
|
Expansion | None
|
|
Expansion | None
|
function that rebuilds |
Expansion | None
|
transformed children. |
coola.recursive.TransformerRegistry ¶
Bases: TypeRegistry[Transformer[Any]]
Registry that manages and dispatches transformers based on data type.
This registry maintains a mapping from Python types to transformer instances and uses the Method Resolution Order (MRO) for type lookup. When transforming data, it automatically selects the most specific registered transformer for the data's type, falling back to parent types or a default transformer if needed.
Type lookups are cached internally for performance. The cache is
cleared automatically whenever the registry is modified (e.g. via
register or register_many).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
initial_state
|
dict[type, T] | None
|
Optional initial mapping of types to transformers. If provided, the state is copied to prevent external mutations. |
None
|
Example
Basic usage with a sequence transformer:
>>> from coola.recursive import TransformerRegistry, SequenceTransformer, DefaultTransformer
>>> registry = TransformerRegistry(
... {object: DefaultTransformer(), list: SequenceTransformer()}
... )
>>> registry
TransformerRegistry(
(<class 'object'>): DefaultTransformer()
(<class 'list'>): SequenceTransformer()
)
>>> registry.transform([1, 2, 3], str)
['1', '2', '3']
Registering custom transformers:
>>> from coola.recursive import TransformerRegistry, SequenceTransformer
>>> registry = TransformerRegistry({object: DefaultTransformer()})
>>> registry.register(list, SequenceTransformer())
>>> registry.transform([1, 2, 3], lambda x: x * 2)
[2, 4, 6]
Working with nested structures:
>>> from coola.recursive import get_default_registry
>>> registry = get_default_registry()
>>> data = {"a": [1, 2], "b": [3, 4]}
>>> registry.transform(data, str)
{'a': ['1', '2'], 'b': ['3', '4']}
coola.recursive.TransformerRegistry.find_transformer ¶
find_transformer(data_type: type) -> Transformer[Any]
Type-specific alias for find: find the appropriate
transformer for a given type.
See TypeRegistry.resolve for the full behavior
description (MRO resolution, caching, and the KeyError on no
match).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_type
|
type
|
The Python type to find a transformer for. |
required |
Returns:
| Type | Description |
|---|---|
Transformer[Any]
|
The most specific registered transformer for this type, resolved |
Transformer[Any]
|
via MRO, or the default transformer if no match is found. |
Example
>>> from collections.abc import Sequence
>>> from coola.recursive import TransformerRegistry, SequenceTransformer, DefaultTransformer
>>> registry = TransformerRegistry({object: DefaultTransformer()})
>>> registry.register(Sequence, SequenceTransformer())
>>> # list does not inherit from Sequence, so it uses DefaultTransformer
>>> transformer = registry.find_transformer(list)
>>> transformer
DefaultTransformer()
coola.recursive.TransformerRegistry.has_transformer ¶
has_transformer(data_type: type) -> bool
Type-specific alias for has: check if a transformer
is explicitly registered for the given type.
See Registry.has for the full behavior
description.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_type
|
type
|
The type to check. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Example
>>> from coola.recursive import TransformerRegistry, SequenceTransformer
>>> registry = TransformerRegistry()
>>> registry.register(list, SequenceTransformer())
>>> registry.has_transformer(list)
True
>>> registry.has_transformer(tuple)
False
coola.recursive.TransformerRegistry.transform ¶
transform(data: object, func: Callable[[Any], Any]) -> Any
Transform data by applying a function recursively through the structure.
This is the main entry point for transformation. It automatically:
- Determines the type of each node.
- Finds the appropriate transformer via
find_transformer(once per type for this call). - Asks that transformer for the children of the node and how to
rebuild it, and applies
functo the leaves.
The traversal uses an explicit stack, so the depth is bounded by the memory only.
The original structure of the data is preserved - only the leaf values are transformed by the provided function.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
object
|
The data to transform. Can be a nested structure such as
a |
required |
func
|
Callable[[Any], Any]
|
Function to apply to leaf values. Should accept one argument and return a transformed value. |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The transformed data, with the same structure as the input but |
Any
|
with leaf values transformed by |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a container contains itself, directly or through other containers (a cycle). |
Example
Converting all numbers to strings in a nested structure:
>>> from coola.recursive import get_default_registry
>>> registry = get_default_registry()
>>> registry.transform({"scores": [95, 87, 92], "name": "test"}, str)
{'scores': ['95', '87', '92'], 'name': 'test'}
Doubling all numeric values:
>>> from coola.recursive import get_default_registry
>>> registry = get_default_registry()
>>> registry.transform(
... [1, [2, 3], {"a": 4}], lambda x: x * 2 if isinstance(x, (int, float)) else x
... )
[2, [4, 6], {'a': 8}]
coola.recursive.get_default_registry ¶
get_default_registry() -> TransformerRegistry
Get or create the default global registry with common Python types.
Returns a singleton registry instance that is pre-configured with transformers for Python's built-in types including sequences (list, tuple), mappings (dict), sets, dataclasses, and scalar types (int, float, str, bool).
This function uses a singleton pattern to ensure the same registry instance is returned on subsequent calls, which is efficient and maintains consistency across an application.
Returns:
| Type | Description |
|---|---|
TransformerRegistry
|
A singleton |
TransformerRegistry
|
scalar and container types. |
Notes
The singleton pattern means modifications to the returned registry affect all future calls to this function. If you need an isolated registry, create a new TransformerRegistry instance directly.
Example
>>> from coola.recursive import get_default_registry
>>> registry = get_default_registry()
>>> # Registry is ready to use with common Python types
>>> registry.transform([1, 2, 3], str)
['1', '2', '3']
>>> registry.transform({"a": 1, "b": 2}, lambda x: x * 10)
{'a': 10, 'b': 20}
coola.recursive.recursive_apply ¶
recursive_apply(
data: object,
func: Callable[[Any], Any],
*,
registry: TransformerRegistry | None = None,
) -> Any
Recursively transform nested data with a type-aware registry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
object
|
Input data, potentially containing nested containers. |
required |
func
|
Callable[[Any], Any]
|
Function applied to leaf values. |
required |
registry
|
TransformerRegistry | None
|
Registry to resolve transformers for nested data.
If |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
Transformed data with the same structure as the input. |
Notes
Mapping keys are preserved. By default, values of str are treated
as leaves and are not iterated character by character.
The function is recursive: each nesting level uses a few Python frames, so a very
deep structure (about a thousand levels) reaches the interpreter recursion limit and
raises a RecursionError that explains how to fix it. A container that contains
itself raises a ValueError.
Example
>>> from coola.recursive import recursive_apply
>>> recursive_apply({"a": 1, "b": "abc"}, str)
{'a': '1', 'b': 'abc'}
>>> # Guard the operation so non-numeric leaves are preserved.
>>> recursive_apply([1, [2, 3], {"x": 4}], lambda x: x * 2 if isinstance(x, int) else x)
[2, [4, 6], {'x': 8}]
coola.recursive.register_transformers ¶
register_transformers(
mapping: Mapping[type, Transformer[Any]],
*,
exist_ok: bool = False,
) -> None
Register custom transformers to the default global registry.
This function extends the singleton registry returned by
get_default_registry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mapping
|
Mapping[type, Transformer[Any]]
|
Mapping of Python types to transformer instances. |
required |
exist_ok
|
bool
|
If |
False
|
Raises:
| Type | Description |
|---|---|
AlreadyRegisteredError
|
If any type is already registered and |
Example
>>> from coola.recursive import register_transformers, BaseTransformer
>>> class MyType:
... def __init__(self, value):
... self.value = value
...
>>> class MyTransformer(BaseTransformer):
... def expand(self, data):
... return [data.value], lambda items: MyType(items[0])
...
>>> register_transformers({MyType: MyTransformer()})