Skip to content

Iterator

coola.iterator

Utilities to traverse and filter nested data structures.

dfs_iterate and bfs_iterate are lazy generators that yield the leaves of a nested structure, once per occurrence and never a container, in depth-first (pre-order) or breadth-first (level order) order. The child finder of an object is chosen from its type along the Method Resolution Order, and one child finder serves both orders. Keys are never yielded, the input is never modified, iterators and generators are leaves (never consumed), and a container that contains itself raises a ValueError. Both orders yield the same leaves; only the order differs, and neither is limited by the depth of the data. dfs_items yields the same leaves with their path.

coola.iterator.BaseChildFinder

Bases: ABC, Generic[T]

Abstract base class for the child finders.

A child finder describes the shape of the values of a type. It is registered in a ChildFinderRegistry and is used by dfs_iterate and bfs_iterate. The generic type parameter T indicates the type of data this child finder handles.

Notes
  • Subclasses must implement the find_children method.
  • find_children is pure and cheap: it does not call the registry, does not recurse, does not modify the data and does not consume the children (it returns them).
  • The result is consumed once, lazily, in order. It can be the container itself, a view (data.values()) or a generator.
  • A leaf returns None: the traversal yields the value itself. A container with no child returns an empty iterable: it is not a leaf and yields nothing.
  • The children are the values to visit: a child finder never returns the keys of a mapping.
Example
>>> from coola.iterator import BaseChildFinder, dfs_iterate, register_child_finders
>>> class Node:
...     def __init__(self, left, right):
...         self.left, self.right = left, right
...
>>> class NodeChildFinder(BaseChildFinder[Node]):
...     def find_children(self, data):
...         return [data.left, data.right]
...
>>> register_child_finders({Node: NodeChildFinder()})
>>> list(dfs_iterate(Node(1, Node(2, 3))))
[1, 2, 3]

coola.iterator.BaseChildFinder.find_children abstractmethod

find_children(data: T) -> Iterable[Any] | None

Find the children of the data.

Parameters:

Name Type Description Default
data T

The data whose children should be found.

required

Returns:

Type Description
Iterable[Any] | None

None if data is a leaf, otherwise the children of data in the order they are visited. An empty iterable for a container without child.

Example
>>> from coola.iterator import DefaultChildFinder, IterableChildFinder
>>> print(DefaultChildFinder().find_children(42))
None
>>> IterableChildFinder().find_children([1, 2])
[1, 2]

coola.iterator.BaseChildFinder.find_items

find_items(data: T) -> Iterable[tuple[Any, Any]] | None

Find the labelled children of the data.

The label of a child is what dfs_items puts in the path of the leaves below it. The default implementation numbers the children returned by find_children (enumerate), so a child finder that only implements find_children works with dfs_items and gets integer labels. Override it to give the raw key, index or name of each child.

Parameters:

Name Type Description Default
data T

The data whose children should be found.

required

Returns:

Type Description
Iterable[tuple[Any, Any]] | None

None if data is a leaf, otherwise the pairs (label, child) in the order they are visited.

Example
>>> from coola.iterator import DefaultChildFinder, MappingChildFinder
>>> print(DefaultChildFinder().find_items(42))
None
>>> list(MappingChildFinder().find_items({"a": 1, "b": 2}))
[('a', 1), ('b', 2)]

coola.iterator.ChildFinder

Bases: Protocol[T_contra]

Protocol of the child finders.

A child finder only describes the shape of a type: it returns the children of a value. The traversal (order, leaves, cycles, depth) is done by dfs_iterate and bfs_iterate, so the same child finder serves both orders.

coola.iterator.ChildFinder.find_children

find_children(data: T_contra) -> Iterable[Any] | None

Find the children of the data.

Parameters:

Name Type Description Default
data T_contra

The data whose children should be found.

required

Returns:

Type Description
Iterable[Any] | None

None if data is a leaf, otherwise the children of data in the order they are visited.

coola.iterator.ChildFinderRegistry

Bases: TypeRegistry[BaseChildFinder[Any]]

Registry that maps Python types to child finders.

During a traversal, the registry selects the most specific child finder for the type of an object using the Method Resolution Order (MRO). The registry is only a dispatcher: the traversal is done by dfs_iterate and bfs_iterate, so one registry serves both orders. The resolved child finders are cached.

Parameters:

Name Type Description Default
initial_state dict[type, T] | None

An optional dictionary mapping Python types to BaseChildFinder instances. If provided, the registry is initialized with this mapping.

None

Attributes:

Name Type Description
_state dict[K, V]

Mapping of registered data types to child finders.

Example
>>> from coola.iterator import (
...     ChildFinderRegistry,
...     DefaultChildFinder,
...     IterableChildFinder,
...     bfs_iterate,
...     dfs_iterate,
... )
>>> registry = ChildFinderRegistry(
...     {object: DefaultChildFinder(), list: IterableChildFinder()}
... )
>>> list(dfs_iterate([1, [2, 3], 4], registry=registry))
[1, 2, 3, 4]
>>> list(bfs_iterate([1, [2, 3], 4], registry=registry))
[1, 4, 2, 3]

coola.iterator.ChildFinderRegistry.find_child_finder

find_child_finder(data_type: type) -> BaseChildFinder[Any]

Type-specific alias for find: find the appropriate child finder for a given data type.

See TypeRegistry.resolve for the full behavior description (MRO resolution, caching, and the error on no match).

Parameters:

Name Type Description Default
data_type type

The data type for which to find a child finder.

required

Returns:

Type Description
BaseChildFinder[Any]

The resolved child finder instance.

Raises:

Type Description
TypeNotRegisteredError

If no child finder is found.

Example
>>> from coola.iterator import (
...     ChildFinderRegistry,
...     DefaultChildFinder,
...     IterableChildFinder,
... )
>>> registry = ChildFinderRegistry(
...     {object: DefaultChildFinder(), list: IterableChildFinder()}
... )
>>> registry.find_child_finder(list)
IterableChildFinder()
>>> registry.find_child_finder(tuple)
DefaultChildFinder()

coola.iterator.ChildFinderRegistry.has_child_finder

has_child_finder(data_type: type) -> bool

Type-specific alias for has: check if a child finder is directly registered for a data 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 child finder is directly registered for the type, False otherwise.

Example
>>> from coola.iterator import ChildFinderRegistry, IterableChildFinder
>>> registry = ChildFinderRegistry({list: IterableChildFinder()})
>>> registry.has_child_finder(list)
True
>>> registry.has_child_finder(tuple)
False

coola.iterator.DataclassChildFinder

Bases: InlineDisplayMixin, BaseChildFinder[Any]

Child finder for dataclass instances.

The children of a dataclass instance are the values of its fields, in the order of the definition of the fields. The label of a child is the name of its field. It is registered as a fallback for dataclasses.is_dataclass in the default registry. A child finder registered for the type of the dataclass has priority; register DefaultChildFinder to keep a dataclass as a leaf.

Example
>>> from dataclasses import dataclass
>>> from coola.iterator import DataclassChildFinder
>>> @dataclass
... class Config:
...     lr: float
...     layers: list
...
>>> list(DataclassChildFinder().find_children(Config(0.1, [1, 2])))
[0.1, [1, 2]]

coola.iterator.DefaultChildFinder

Bases: InlineDisplayMixin, BaseChildFinder[Any]

Default child finder: the data is a leaf.

This child finder is the fallback for the objects that do not have a specialized child finder registered. It is used for the primitive types (int, float, str, bool, ...), the iterators and every object without a structure to traverse.

Example
>>> from coola.iterator import DefaultChildFinder
>>> child_finder = DefaultChildFinder()
>>> print(child_finder.find_children(42))
None
>>> print(child_finder.find_children("hello"))
None

coola.iterator.IterableChildFinder

Bases: InlineDisplayMixin, BaseChildFinder[Iterable[Any]]

Child finder for the iterable containers.

The container is its own iterable of children: its elements are visited in the order of its iteration. It works with lists, tuples, sets and any object implementing the Iterable protocol. The label of an element is its index, or the element itself for a set (a set has no position).

Example
>>> from coola.iterator import IterableChildFinder
>>> child_finder = IterableChildFinder()
>>> list(child_finder.find_children((4, 2, 1)))
[4, 2, 1]
>>> list(child_finder.find_children([]))
[]

coola.iterator.MappingChildFinder

Bases: InlineDisplayMixin, BaseChildFinder[Mapping[Any, Any]]

Child finder for the mappings.

The children of a mapping are its values, in the order of its iteration. The keys are never visited. It works with dictionaries and any object implementing the Mapping protocol.

Example
>>> from coola.iterator import MappingChildFinder
>>> child_finder = MappingChildFinder()
>>> list(child_finder.find_children({"a": 1, "b": 2}))
[1, 2]
>>> list(child_finder.find_children({"a": {"b": 1, "c": 2}, "d": 3}))
[{'b': 1, 'c': 2}, 3]

coola.iterator.bfs_iterate

bfs_iterate(
    data: object,
    *,
    registry: ChildFinderRegistry | None = None,
) -> Iterator[Any]

Iterate over the leaves of nested data in breadth-first order.

The traversal is level order, left to right inside a level. It visits the same leaves as dfs_iterate; only the order differs. The child finder of each object is chosen from its type with registry. Only the leaves are yielded, never a container.

The function is a generator: the leaves are produced on demand. It uses an explicit queue, so there is no limit on the depth of the data. The memory is proportional to the width of a level.

Parameters:

Name Type Description Default
data object

The nested data to traverse.

required
registry ChildFinderRegistry | None

The registry used to resolve the child finders. If None, the default registry is used.

None

Yields:

Type Description
Any

The leaves of data in breadth-first order.

Raises:

Type Description
ValueError

If a container contains itself, when the traversal reaches it (the leaves before it are already yielded).

Example
>>> from coola.iterator import bfs_iterate
>>> list(bfs_iterate({"a": 1, "b": "abc"}))
[1, 'abc']
>>> list(bfs_iterate([1, [2, [3]], 4]))
[1, 4, 2, 3]

coola.iterator.dfs_items

dfs_items(
    data: object,
    *,
    registry: ChildFinderRegistry | None = None,
    empty: bool = False,
) -> Iterator[tuple[tuple[Any, ...], Any]]

Iterate over the leaves of nested data with their path, in depth- first order.

It yields the leaves of dfs_iterate in the same order, each with its path: the tuple of the labels of the containers crossed to reach it. The label of a child is given by the find_items method of its container's child finder: the key of a mapping, the index of a sequence, the name of the field of a dataclass, the element itself for a set. The labels are raw (a key is not converted to str). The root being a leaf gives the empty path.

The function is a generator and uses an explicit stack, so there is no limit on the depth of the data.

Parameters:

Name Type Description Default
data object

The nested data to traverse.

required
registry ChildFinderRegistry | None

The registry used to resolve the child finders. If None, the default registry is used.

None
empty bool

If True, an empty container is also yielded, as a leaf ((path, container)). By default an empty container yields nothing.

False

Yields:

Type Description
tuple[tuple[Any, ...], Any]

The pairs (path, leaf) of data in depth-first order.

Raises:

Type Description
ValueError

If a container contains itself, when the traversal reaches it (the leaves before it are already yielded).

Example
>>> from coola.iterator import dfs_items
>>> list(dfs_items({"a": 1, "b": [2, {"c": 3}]}))
[(('a',), 1), (('b', 0), 2), (('b', 1, 'c'), 3)]
>>> list(dfs_items({"a": {}}))
[]
>>> list(dfs_items({"a": {}}, empty=True))
[(('a',), {})]

coola.iterator.dfs_iterate

dfs_iterate(
    data: object,
    *,
    registry: ChildFinderRegistry | None = None,
) -> Iterator[Any]

Iterate over the leaves of nested data in depth-first order.

The traversal is pre-order, left to right. The child finder of each object is chosen from its type with registry. Only the leaves are yielded, once per occurrence, never a container (an empty container yields nothing). The keys of a mapping are never yielded. Iterators and generators are leaves: they are never consumed.

The function is a generator: the leaves are produced on demand and nothing is walked before the first next. It uses an explicit stack, so there is no limit on the depth of the data.

Parameters:

Name Type Description Default
data object

The nested data to traverse.

required
registry ChildFinderRegistry | None

The registry used to resolve the child finders. If None, the default registry is used.

None

Yields:

Type Description
Any

The leaves of data in depth-first order.

Raises:

Type Description
ValueError

If a container contains itself, when the traversal reaches it (the leaves before it are already yielded).

Example
>>> from coola.iterator import dfs_iterate
>>> list(dfs_iterate({"a": 1, "b": "abc"}))
[1, 'abc']
>>> list(dfs_iterate([1, [2, 3], {"x": 4}]))
[1, 2, 3, 4]

coola.iterator.filter_by_type

filter_by_type(
    iterator: Iterable[Any],
    types: type[T] | tuple[type[T], ...],
) -> Iterator[T]

Filter an iterator to yield only values of specified types.

This function acts as a type-safe filter that passes through only values matching the specified type(s). It's particularly useful when working with heterogeneous collections where you need to extract elements of specific types.

Parameters:

Name Type Description Default
iterator Iterable[Any]

An iterator or iterable to filter. Can contain values of any type.

required
types type[T] | tuple[type[T], ...]

A single type or tuple of types to filter for. Only values that are instances of these types will be yielded. Follows the same semantics as the built-in isinstance() function.

required

Yields:

Type Description
T

Values from the input iterator that are instances of any of the specified

T

types, preserving the original order of elements.

Example

Filter mixed-type list to get only integers:

>>> from coola.iterator import filter_by_type
>>> list(filter_by_type([1, "hello", 2, 3.14, "world", 4], int))
[1, 2, 4]

Filter for multiple types:

>>> from coola.iterator import filter_by_type
>>> # Note: bool is a subclass of int
>>> list(filter_by_type([1, "hello", 2.5, True, None, [1, 2]], (int, float)))
[1, 2.5, True]
Notes
  • This function uses isinstance() internally, so subclass relationships are respected (e.g., bool values will match int type).
  • With a tuple of types, the type checker infers the union of the types (Iterator[int | float] for (int, float)).
  • The input iterator is consumed as items are yielded.
  • For empty iterators or when no items match, the generator yields nothing.

coola.iterator.get_default_registry

get_default_registry() -> ChildFinderRegistry

Get or create the default global registry for child finders.

This function returns a singleton instance of the ChildFinderRegistry, which is pre-configured with child finders for the common Python types: the sequences, the sets, the mappings and the dataclasses are entered, the scalars and the other objects are leaves.

Returns:

Type Description
ChildFinderRegistry

An ChildFinderRegistry instance with child finders registered for common Python types.

Notes

The singleton pattern means any changes to the returned registry affect all future calls to this function. If an isolated registry is needed, create a new ChildFinderRegistry instance directly.

Example
>>> from coola.iterator import get_default_registry
>>> registry = get_default_registry()
>>> registry.has_child_finder(list)
True

coola.iterator.register_child_finders

register_child_finders(
    mapping: Mapping[type, BaseChildFinder[Any]],
    *,
    exist_ok: bool = False,
) -> None

Register custom child finders to the default global registry.

One registration serves both dfs_iterate and bfs_iterate.

Parameters:

Name Type Description Default
mapping Mapping[type, BaseChildFinder[Any]]

A dictionary mapping Python types to their corresponding child finder instances.

required
exist_ok bool

If True, existing registrations for types will be overwritten. If False, an error is raised when a type is already registered.

False
Example
>>> from coola.iterator import IterableChildFinder, register_child_finders
>>> register_child_finders({list: IterableChildFinder()}, exist_ok=True)