Skip to content

Recursive

coola.recursive

Utilities to recursively transform nested data structures.

Properties:

  • Leaves only: func is 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 func propagates 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:

  1. It is pure and cheap: it does not call the function, does not call the registry, does not recurse and does not modify data.
  2. rebuild receives a new list with the transformed children, in the order of children, and returns a new container (it returns data itself only if it has no children and must be kept as it is, like IdentityTransformer).
  3. A container with no child ([], {}) returns empty children: rebuild([]) builds an empty container of the same type.
  4. 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

None if data is a leaf. Otherwise, a tuple

Expansion | None

(children, rebuild): the children of data and the

Expansion | None

function that rebuilds data from a list with the

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_dataclass in the default registry. A transformer registered for the type of the dataclass has priority; register DefaultTransformer to 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 data.

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

None if the object is a leaf. Otherwise the children and the function that rebuilds the set from the transformed children.

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

None if data is a leaf. Otherwise, a tuple

Expansion | None

(children, rebuild): the children of data and the

Expansion | None

function that rebuilds data from a list with the

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

True if a transformer is explicitly registered for this type, False otherwise.

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:

  1. Determines the type of each node.
  2. Finds the appropriate transformer via find_transformer (once per type for this call).
  3. Asks that transformer for the children of the node and how to rebuild it, and applies func to 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 list, dict, or tuple.

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 func.

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 configured for common built-in

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, the default registry is used.

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 (default), raise an error when a type is already registered. If True, overwrites existing registrations silently.

False

Raises:

Type Description
AlreadyRegisteredError

If any type is already registered and exist_ok is False.

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()})