Skip to content

API Reference

The public API of pyhrp. Import the main entry points directly from the top-level package:

from pyhrp.hrp import hrp, build_tree, Dendrogram
from pyhrp.algos import risk_parity, one_over_n
from pyhrp.cluster import Cluster, Portfolio

pyhrp.hrp

Hierarchical Risk Parity (HRP) algorithm implementation.

This module implements the core HRP algorithm and related functions: - hrp: Main function to compute HRP portfolio weights - build_tree: Function to build hierarchical cluster tree from correlation matrix - Dendrogram: Class to store and visualize hierarchical clustering results

Dendrogram dataclass

Container for hierarchical clustering dendrogram data and visualization.

This class stores the results of hierarchical clustering and provides methods for accessing and visualizing the dendrogram structure.

Attributes:

Name Type Description
root Cluster

The root node of the hierarchical clustering tree

assets Index

Index of assets included in the clustering

linkage ndarray | None

Linkage matrix in scipy format for plotting

distance ndarray | None

Distance matrix used for clustering

method str | None

Linkage method used for clustering

Source code in src/pyhrp/hrp.py
@dataclass(frozen=True)
class Dendrogram:
    """Container for hierarchical clustering dendrogram data and visualization.

    This class stores the results of hierarchical clustering and provides methods
    for accessing and visualizing the dendrogram structure.

    Attributes:
        root (Cluster): The root node of the hierarchical clustering tree
        assets (pd.Index): Index of assets included in the clustering
        linkage (np.ndarray | None): Linkage matrix in scipy format for plotting
        distance (np.ndarray | None): Distance matrix used for clustering
        method (str | None): Linkage method used for clustering
    """

    root: Cluster
    assets: pd.Index
    distance: pd.DataFrame | None = None
    linkage: np.ndarray | None = None
    method: str | None = None

    def __post_init__(self) -> None:
        """Validate dataclass fields after initialization.

        Ensures that the optional distance matrix, when provided, is a pandas
        DataFrame aligned with the asset order, and verifies that the number of
        leaves in the cluster tree matches the number of assets.
        """
        # ---- Optional: validate distance index/columns ----
        if self.distance is not None:
            if not isinstance(self.distance, pd.DataFrame):
                raise TypeError("distance must be a pandas DataFrame.")  # noqa: TRY003

            # Optionally check if distance matches assets
            if not self.distance.index.equals(pd.Index(self.assets)) or not self.distance.columns.equals(
                pd.Index(self.assets)
            ):
                raise ValueError("Distance matrix index/columns must align with assets.")  # noqa: TRY003

        # Check the number of leaves and assets
        if len(self.root.leaves) != len(self.assets):
            raise ValueError("Number of leaves does not match number of assets.")  # noqa: TRY003

    def plot(self, **kwargs: object) -> None:
        """Plot the dendrogram."""
        sch.dendrogram(self.linkage, leaf_rotation=90, leaf_font_size=8, labels=self.assets, **kwargs)

    @property
    def ids(self) -> list[int]:
        """Node values in the order left -> right as they appear in the dendrogram."""
        return [node.value for node in self.root.leaves]  # type: ignore[misc]

    @property
    def names(self) -> list[str]:
        """The asset names as induced by the order of ids."""
        return [self.assets[i] for i in self.ids]

ids property

Node values in the order left -> right as they appear in the dendrogram.

names property

The asset names as induced by the order of ids.

__post_init__()

Validate dataclass fields after initialization.

Ensures that the optional distance matrix, when provided, is a pandas DataFrame aligned with the asset order, and verifies that the number of leaves in the cluster tree matches the number of assets.

Source code in src/pyhrp/hrp.py
def __post_init__(self) -> None:
    """Validate dataclass fields after initialization.

    Ensures that the optional distance matrix, when provided, is a pandas
    DataFrame aligned with the asset order, and verifies that the number of
    leaves in the cluster tree matches the number of assets.
    """
    # ---- Optional: validate distance index/columns ----
    if self.distance is not None:
        if not isinstance(self.distance, pd.DataFrame):
            raise TypeError("distance must be a pandas DataFrame.")  # noqa: TRY003

        # Optionally check if distance matches assets
        if not self.distance.index.equals(pd.Index(self.assets)) or not self.distance.columns.equals(
            pd.Index(self.assets)
        ):
            raise ValueError("Distance matrix index/columns must align with assets.")  # noqa: TRY003

    # Check the number of leaves and assets
    if len(self.root.leaves) != len(self.assets):
        raise ValueError("Number of leaves does not match number of assets.")  # noqa: TRY003

plot(**kwargs)

Plot the dendrogram.

Source code in src/pyhrp/hrp.py
def plot(self, **kwargs: object) -> None:
    """Plot the dendrogram."""
    sch.dendrogram(self.linkage, leaf_rotation=90, leaf_font_size=8, labels=self.assets, **kwargs)

build_tree(cor, method='ward', bisection=False)

Build hierarchical cluster tree from correlation matrix.

This function converts a correlation matrix to a distance matrix, performs hierarchical clustering, and returns a Dendrogram object containing the resulting tree structure.

Parameters:

Name Type Description Default
cor DataFrame

Correlation matrix of asset returns

required
method Literal['single', 'complete', 'average', 'ward']

Linkage method for hierarchical clustering - "single": minimum distance between points (nearest neighbor) - "complete": maximum distance between points (furthest neighbor) - "average": average distance between all points - "ward": Ward variance minimization

'ward'
bisection bool

Whether to use bisection method for tree construction

False

Returns:

Name Type Description
Dendrogram Dendrogram

Object containing the hierarchical clustering tree, with: - root: Root cluster node - linkage: Linkage matrix for plotting - assets: List of assets - method: Clustering method used - distance: Distance matrix

Source code in src/pyhrp/hrp.py
def build_tree(
    cor: pd.DataFrame, method: Literal["single", "complete", "average", "ward"] = "ward", bisection: bool = False
) -> Dendrogram:
    """Build hierarchical cluster tree from correlation matrix.

    This function converts a correlation matrix to a distance matrix, performs
    hierarchical clustering, and returns a Dendrogram object containing the
    resulting tree structure.

    Args:
        cor (pd.DataFrame): Correlation matrix of asset returns
        method (Literal["single", "complete", "average", "ward"]): Linkage method for hierarchical clustering
            - "single": minimum distance between points (nearest neighbor)
            - "complete": maximum distance between points (furthest neighbor)
            - "average": average distance between all points
            - "ward": Ward variance minimization
        bisection (bool): Whether to use bisection method for tree construction

    Returns:
        Dendrogram: Object containing the hierarchical clustering tree, with:
            - root: Root cluster node
            - linkage: Linkage matrix for plotting
            - assets: List of assets
            - method: Clustering method used
            - distance: Distance matrix
    """
    # Create distance matrix and linkage
    if not isinstance(cor, pd.DataFrame):
        raise TypeError("Correlation matrix must be a pandas DataFrame.")  # noqa: TRY003
    dist = _compute_distance_matrix(cor)
    links = sch.linkage(ssd.squareform(dist), method=method)

    # Convert scipy tree to our Cluster format
    def to_cluster(node: sch.ClusterNode) -> Cluster:
        """Convert a scipy ClusterNode to our Cluster format.

        Args:
            node (sch.ClusterNode): A node from scipy's hierarchical clustering

        Returns:
            Cluster: Equivalent node in our Cluster format
        """
        if node.left is not None and node.right is not None:
            left = to_cluster(node.left)
            right = to_cluster(node.right)
            return Cluster(value=node.id, left=left, right=right)
        return Cluster(value=node.id)

    root = to_cluster(sch.to_tree(links, rd=False))

    # Apply bisection if requested
    if bisection:
        # Rebuild tree using bisection
        leaf_ids: list[int] = [int(node.value) for node in root.leaves]
        nnn: int = max(leaf_ids)

        def bisect_tree(ids: list[int]) -> Cluster:
            """Build tree by recursive bisection.

            This function recursively splits the list of IDs in half and creates
            a binary tree where each node represents a split.

            Args:
                ids (list[int]): List of leaf node IDs to organize into a tree

            Returns:
                Cluster: Root node of the constructed tree
            """
            nonlocal nnn

            if len(ids) == 1:
                return Cluster(value=ids[0])

            mid = len(ids) // 2
            left_ids, right_ids = ids[:mid], ids[mid:]

            left = bisect_tree(left_ids)
            right = bisect_tree(right_ids)

            nnn += 1
            return Cluster(value=nnn, left=left, right=right)

        root = bisect_tree(leaf_ids)

        # Convert back to linkage format for plotting
        links_list: list[list[float]] = []

        def get_linkage(node: Cluster) -> None:
            """Convert tree structure back to linkage matrix format.

            This function traverses the tree and builds a linkage matrix compatible
            with scipy's hierarchical clustering format for visualization.

            Args:
                node (Cluster): Current node being processed
            """
            if node.left is not None and node.right is not None:
                if not isinstance(node.left, Cluster):
                    raise TypeError("Expected left child to be a Cluster")  # noqa: TRY003  # pragma: no cover
                if not isinstance(node.right, Cluster):
                    raise TypeError("Expected right child to be a Cluster")  # noqa: TRY003  # pragma: no cover
                get_linkage(node.left)
                get_linkage(node.right)
                links_list.append(
                    [
                        float(node.left.value),
                        float(node.right.value),
                        float(node.size),
                        float(len(node.left.leaves) + len(node.right.leaves)),
                    ]
                )

        get_linkage(root)
        links = np.array(links_list)

    return Dendrogram(root=root, linkage=links, method=method, distance=dist, assets=cor.columns)

hrp(prices, node=None, method='ward', bisection=False)

Compute the hierarchical risk parity portfolio weights.

This is the main entry point for the HRP algorithm. It calculates returns from prices, builds a hierarchical clustering tree if not provided, and applies risk parity weights.

Parameters:

Name Type Description Default
prices DataFrame

Asset price time series

required
node Cluster

Root node of the hierarchical clustering tree. If None, a tree will be built from the correlation matrix.

None
method Literal['single', 'complete', 'average', 'ward']

Linkage method to use for distance calculation - "single": minimum distance between points (nearest neighbor) - "complete": maximum distance between points (furthest neighbor) - "average": average distance between all points - "ward": Ward variance minimization

'ward'
bisection bool

Whether to use bisection method for tree construction

False

Returns:

Name Type Description
Cluster Cluster

The root cluster with portfolio weights assigned according to HRP

Source code in src/pyhrp/hrp.py
def hrp(
    prices: pd.DataFrame,
    node: Cluster | None = None,
    method: Literal["single", "complete", "average", "ward"] = "ward",
    bisection: bool = False,
) -> Cluster:
    """Compute the hierarchical risk parity portfolio weights.

    This is the main entry point for the HRP algorithm. It calculates returns from prices,
    builds a hierarchical clustering tree if not provided, and applies risk parity weights.

    Args:
        prices (pd.DataFrame): Asset price time series
        node (Cluster, optional): Root node of the hierarchical clustering tree.
            If None, a tree will be built from the correlation matrix.
        method (Literal["single", "complete", "average", "ward"]): Linkage method to use for distance calculation
            - "single": minimum distance between points (nearest neighbor)
            - "complete": maximum distance between points (furthest neighbor)
            - "average": average distance between all points
            - "ward": Ward variance minimization
        bisection (bool): Whether to use bisection method for tree construction

    Returns:
        Cluster: The root cluster with portfolio weights assigned according to HRP
    """
    returns = prices.pct_change().dropna(axis=0, how="all")
    cov, cor = returns.cov(), returns.corr()
    node = node or build_tree(cor, method=method, bisection=bisection).root

    return risk_parity(root=node, cov=cov)

pyhrp.algos

Portfolio optimization algorithms for hierarchical risk parity.

This module implements various portfolio optimization algorithms: - risk_parity: The main hierarchical risk parity algorithm - one_over_n: A simple equal-weight allocation strategy

one_over_n(dendrogram)

Generate portfolios using the 1/N (equal weight) strategy at each tree level.

This function implements a hierarchical 1/N strategy where weights are distributed equally among assets within each cluster at each level of the tree. The weight assigned to each cluster decreases by half at each level.

Parameters:

Name Type Description Default
dendrogram Any

A dendrogram object containing the hierarchical clustering tree and the list of assets

required

Yields:

Type Description
Generator[tuple[int, Portfolio]]

tuple[int, Portfolio]: A tuple containing the level number and the portfolio at that level

Source code in src/pyhrp/algos.py
def one_over_n(dendrogram: Any) -> Generator[tuple[int, Portfolio]]:
    """Generate portfolios using the 1/N (equal weight) strategy at each tree level.

    This function implements a hierarchical 1/N strategy where weights are
    distributed equally among assets within each cluster at each level of the tree.
    The weight assigned to each cluster decreases by half at each level.

    Args:
        dendrogram: A dendrogram object containing the hierarchical clustering tree
                   and the list of assets

    Yields:
        tuple[int, Portfolio]: A tuple containing the level number and the portfolio
                              at that level
    """
    root = dendrogram.root
    assets = dendrogram.assets

    # Initial weight to distribute
    w: float = 1.0

    # Process each level of the tree
    for n, level in enumerate(root.levels):
        for node in level:
            # Distribute weight equally among all leaves in this node
            for leaf in node.leaves:
                root.portfolio[assets[leaf.value]] = w / node.leaf_count

        # Reduce weight for the next level
        w *= 0.5

        # Yield the current level number and a deep copy of the portfolio
        yield n, deepcopy(root.portfolio)

risk_parity(root, cov)

Compute hierarchical risk parity weights for a cluster tree.

This is the main algorithm for hierarchical risk parity. It recursively traverses the cluster tree and assigns weights to each node based on the risk parity principle.

Parameters:

Name Type Description Default
root Cluster

The root node of the cluster tree

required
cov DataFrame

Covariance matrix of asset returns

required

Returns:

Name Type Description
Cluster Cluster

The root node with portfolio weights assigned

Source code in src/pyhrp/algos.py
def risk_parity(root: Cluster, cov: pd.DataFrame) -> Cluster:
    """Compute hierarchical risk parity weights for a cluster tree.

    This is the main algorithm for hierarchical risk parity. It recursively
    traverses the cluster tree and assigns weights to each node based on
    the risk parity principle.

    Args:
        root (Cluster): The root node of the cluster tree
        cov (pd.DataFrame): Covariance matrix of asset returns

    Returns:
        Cluster: The root node with portfolio weights assigned
    """
    if root.is_leaf:
        # a node is a leaf if has no further relatives downstream.
        asset = cov.keys().to_list()[int(root.value)]
        root.portfolio[asset] = 1.0
        return root

    # drill down on the left
    if not isinstance(root.left, Cluster):
        raise TypeError("Expected left child to be a Cluster")  # noqa: TRY003
    if not isinstance(root.right, Cluster):
        raise TypeError("Expected right child to be a Cluster")  # noqa: TRY003
    root.left = risk_parity(root.left, cov)
    # drill down on the right
    root.right = risk_parity(root.right, cov)

    # combine left and right into a new cluster
    return _parity(root, cov=cov)

pyhrp.cluster

Data structures for hierarchical risk parity portfolio optimization.

This module defines the core data structures used in the hierarchical risk parity algorithm: - Portfolio: Manages a collection of asset weights (strings identify assets) - Cluster: Represents a node in the hierarchical clustering tree

Cluster

Bases: Node

Represents a cluster in the hierarchical clustering tree.

Clusters are the nodes of the graphs we build. Each cluster is aware of the left and the right cluster it is connecting to. Each cluster also has an associated portfolio.

Attributes:

Name Type Description
portfolio Portfolio

The portfolio associated with this cluster

Source code in src/pyhrp/cluster.py
class Cluster(Node):
    """Represents a cluster in the hierarchical clustering tree.

    Clusters are the nodes of the graphs we build.
    Each cluster is aware of the left and the right cluster
    it is connecting to. Each cluster also has an associated portfolio.

    Attributes:
        portfolio (Portfolio): The portfolio associated with this cluster
    """

    def __init__(self, value: int, left: Cluster | None = None, right: Cluster | None = None, **kwargs: Any) -> None:
        """Initialize a new Cluster.

        Args:
            value (int): The identifier for this cluster
            left (Cluster, optional): The left child cluster
            right (Cluster, optional): The right child cluster
            **kwargs: Additional arguments to pass to the parent class
        """
        super().__init__(value=value, left=left, right=right, **kwargs)
        self.portfolio = Portfolio()

    @property
    def is_leaf(self) -> bool:
        """Check if this cluster is a leaf node (has no children).

        Returns:
            bool: True if this is a leaf node, False otherwise
        """
        return self.left is None and self.right is None

    @property
    def leaves(self) -> list[Cluster]:
        """Get all reachable leaf nodes in the correct order.

        Note that the leaves method of the Node class implemented in BinaryTree
        is not respecting the 'correct' order of the nodes.

        Returns:
            list[Cluster]: List of all leaf nodes reachable from this cluster
        """
        if self.is_leaf:
            return [self]
        else:
            if self.left is None:
                raise ValueError("Expected left child to exist for non-leaf cluster")  # noqa: TRY003
            if self.right is None:
                raise ValueError("Expected right child to exist for non-leaf cluster")  # noqa: TRY003
            left_leaves: list[Cluster] = self.left.leaves  # type: ignore[assignment]
            right_leaves: list[Cluster] = self.right.leaves  # type: ignore[assignment]
            return left_leaves + right_leaves

is_leaf property

Check if this cluster is a leaf node (has no children).

Returns:

Name Type Description
bool bool

True if this is a leaf node, False otherwise

leaves property

Get all reachable leaf nodes in the correct order.

Note that the leaves method of the Node class implemented in BinaryTree is not respecting the 'correct' order of the nodes.

Returns:

Type Description
list[Cluster]

list[Cluster]: List of all leaf nodes reachable from this cluster

__init__(value, left=None, right=None, **kwargs)

Initialize a new Cluster.

Parameters:

Name Type Description Default
value int

The identifier for this cluster

required
left Cluster

The left child cluster

None
right Cluster

The right child cluster

None
**kwargs Any

Additional arguments to pass to the parent class

{}
Source code in src/pyhrp/cluster.py
def __init__(self, value: int, left: Cluster | None = None, right: Cluster | None = None, **kwargs: Any) -> None:
    """Initialize a new Cluster.

    Args:
        value (int): The identifier for this cluster
        left (Cluster, optional): The left child cluster
        right (Cluster, optional): The right child cluster
        **kwargs: Additional arguments to pass to the parent class
    """
    super().__init__(value=value, left=left, right=right, **kwargs)
    self.portfolio = Portfolio()

Portfolio dataclass

Container for portfolio asset weights.

This lightweight class stores and manipulates a mapping from asset names to their portfolio weights, and provides convenience helpers for analysis and visualization.

Attributes:

Name Type Description
_weights dict[str, float]

Internal mapping from asset symbol to weight.

Source code in src/pyhrp/cluster.py
@dataclass
class Portfolio:
    """Container for portfolio asset weights.

    This lightweight class stores and manipulates a mapping from asset names to
    their portfolio weights, and provides convenience helpers for analysis and
    visualization.

    Attributes:
        _weights (dict[str, float]): Internal mapping from asset symbol to weight.
    """

    _weights: dict[str, float] = field(default_factory=dict)

    @property
    def assets(self) -> list[str]:
        """List of asset names present in the portfolio.

        Returns:
            list[str]: Asset identifiers in insertion order (Python 3.7+ dict order).
        """
        return list(self._weights.keys())

    def variance(self, cov: pd.DataFrame) -> float:
        """Calculate the variance of the portfolio.

        Args:
            cov (pd.DataFrame): Covariance matrix

        Returns:
            float: Portfolio variance
        """
        c = cov[self.assets].loc[self.assets].values
        w = self.weights[self.assets].values
        return float(np.linalg.multi_dot((w, c, w)))

    def __getitem__(self, item: str) -> float:
        """Return the weight for a given asset.

        Args:
            item (str): Asset name/symbol.

        Returns:
            float: The weight associated with the asset.

        Raises:
            KeyError: If the asset is not present in the portfolio.
        """
        return self._weights[item]

    def __setitem__(self, key: str, value: float) -> None:
        """Set or update the weight for an asset.

        Args:
            key (str): Asset name/symbol.
            value (float): Portfolio weight for the asset.
        """
        self._weights[key] = value

    @property
    def weights(self) -> pd.Series:
        """Get all weights as a pandas Series.

        Returns:
            pd.Series: Series of weights indexed by assets
        """
        return pd.Series(self._weights, name="Weights").sort_index()

    def plot(self, names: list[str]) -> Axes:
        """Plot the portfolio weights.

        Args:
            names (list[str]): List of asset names to include in the plot

        Returns:
            matplotlib.axes.Axes: The plot axes
        """
        a = self.weights.loc[names]

        ax = a.plot(kind="bar", color="skyblue")

        # Set x-axis labels and rotations
        ax.set_xticklabels(names, rotation=90, fontsize=8)
        return ax

assets property

List of asset names present in the portfolio.

Returns:

Type Description
list[str]

list[str]: Asset identifiers in insertion order (Python 3.7+ dict order).

weights property

Get all weights as a pandas Series.

Returns:

Type Description
Series

pd.Series: Series of weights indexed by assets

__getitem__(item)

Return the weight for a given asset.

Parameters:

Name Type Description Default
item str

Asset name/symbol.

required

Returns:

Name Type Description
float float

The weight associated with the asset.

Raises:

Type Description
KeyError

If the asset is not present in the portfolio.

Source code in src/pyhrp/cluster.py
def __getitem__(self, item: str) -> float:
    """Return the weight for a given asset.

    Args:
        item (str): Asset name/symbol.

    Returns:
        float: The weight associated with the asset.

    Raises:
        KeyError: If the asset is not present in the portfolio.
    """
    return self._weights[item]

__setitem__(key, value)

Set or update the weight for an asset.

Parameters:

Name Type Description Default
key str

Asset name/symbol.

required
value float

Portfolio weight for the asset.

required
Source code in src/pyhrp/cluster.py
def __setitem__(self, key: str, value: float) -> None:
    """Set or update the weight for an asset.

    Args:
        key (str): Asset name/symbol.
        value (float): Portfolio weight for the asset.
    """
    self._weights[key] = value

plot(names)

Plot the portfolio weights.

Parameters:

Name Type Description Default
names list[str]

List of asset names to include in the plot

required

Returns:

Type Description
Axes

matplotlib.axes.Axes: The plot axes

Source code in src/pyhrp/cluster.py
def plot(self, names: list[str]) -> Axes:
    """Plot the portfolio weights.

    Args:
        names (list[str]): List of asset names to include in the plot

    Returns:
        matplotlib.axes.Axes: The plot axes
    """
    a = self.weights.loc[names]

    ax = a.plot(kind="bar", color="skyblue")

    # Set x-axis labels and rotations
    ax.set_xticklabels(names, rotation=90, fontsize=8)
    return ax

variance(cov)

Calculate the variance of the portfolio.

Parameters:

Name Type Description Default
cov DataFrame

Covariance matrix

required

Returns:

Name Type Description
float float

Portfolio variance

Source code in src/pyhrp/cluster.py
def variance(self, cov: pd.DataFrame) -> float:
    """Calculate the variance of the portfolio.

    Args:
        cov (pd.DataFrame): Covariance matrix

    Returns:
        float: Portfolio variance
    """
    c = cov[self.assets].loc[self.assets].values
    w = self.weights[self.assets].values
    return float(np.linalg.multi_dot((w, c, w)))

pyhrp.treelib

A lightweight binary tree implementation to replace the binarytree dependency.

This module provides a simple Node class that can be used to create binary trees. It implements only the functionality needed by the pyhrp package.

Node

A binary tree node with left and right children.

This class implements the minimal functionality needed from the binarytree.Node class that is used in the pyhrp package.

Attributes:

Name Type Description
value

The value of the node

left

The left child node

right

The right child node

Source code in src/pyhrp/treelib.py
class Node:
    """A binary tree node with left and right children.

    This class implements the minimal functionality needed from the binarytree.Node class
    that is used in the pyhrp package.

    Attributes:
        value: The value of the node
        left: The left child node
        right: The right child node
    """

    def __init__(self, value: NodeValue, left: Node | None = None, right: Node | None = None):
        """Initialize a new Node.

        Args:
            value: The value of the node
            left: The left child node
            right: The right child node
        """
        self.value = value
        self.left = left
        self.right = right

    @property
    def is_leaf(self) -> bool:
        """Check if this node is a leaf node (has no children).

        Returns:
            bool: True if this is a leaf node, False otherwise
        """
        return self.left is None and self.right is None

    @property
    def leaves(self) -> list[Node]:
        """Get all leaf nodes in the tree rooted at this node.

        Returns:
            List[Node]: List of all leaf nodes
        """
        if self.is_leaf:
            return [self]

        result = []
        if self.left:
            result.extend(self.left.leaves)
        if self.right:
            result.extend(self.right.leaves)

        return result

    @property
    def levels(self) -> list[list[Node]]:
        """Get nodes by level in the tree.

        Returns:
            List[List[Node]]: List of lists of nodes at each level
        """
        result = []
        current_level = [self]

        while current_level:
            result.append(current_level)
            next_level = []

            for node in current_level:
                if node.left:
                    next_level.append(node.left)
                if node.right:
                    next_level.append(node.right)

            current_level = next_level

        return result

    @property
    def leaf_count(self) -> int:
        """Count the number of leaf nodes in the tree.

        Returns:
            int: Number of leaf nodes
        """
        return len(self.leaves)

    @property
    def size(self) -> int:
        """Count the total number of nodes in the tree.

        Returns:
            int: Total number of nodes
        """
        size = 1  # Count this node
        if self.left:
            size += self.left.size
        if self.right:
            size += self.right.size
        return size

    def __iter__(self) -> Iterator[Node]:
        """Iterate through all nodes in the tree in level-order.

        Returns:
            Iterator[Node]: Iterator over all nodes
        """
        queue: deque[Node] = deque([self])
        while queue:
            node = queue.popleft()
            yield node
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)

is_leaf property

Check if this node is a leaf node (has no children).

Returns:

Name Type Description
bool bool

True if this is a leaf node, False otherwise

leaf_count property

Count the number of leaf nodes in the tree.

Returns:

Name Type Description
int int

Number of leaf nodes

leaves property

Get all leaf nodes in the tree rooted at this node.

Returns:

Type Description
list[Node]

List[Node]: List of all leaf nodes

levels property

Get nodes by level in the tree.

Returns:

Type Description
list[list[Node]]

List[List[Node]]: List of lists of nodes at each level

size property

Count the total number of nodes in the tree.

Returns:

Name Type Description
int int

Total number of nodes

__init__(value, left=None, right=None)

Initialize a new Node.

Parameters:

Name Type Description Default
value NodeValue

The value of the node

required
left Node | None

The left child node

None
right Node | None

The right child node

None
Source code in src/pyhrp/treelib.py
def __init__(self, value: NodeValue, left: Node | None = None, right: Node | None = None):
    """Initialize a new Node.

    Args:
        value: The value of the node
        left: The left child node
        right: The right child node
    """
    self.value = value
    self.left = left
    self.right = right

__iter__()

Iterate through all nodes in the tree in level-order.

Returns:

Type Description
Iterator[Node]

Iterator[Node]: Iterator over all nodes

Source code in src/pyhrp/treelib.py
def __iter__(self) -> Iterator[Node]:
    """Iterate through all nodes in the tree in level-order.

    Returns:
        Iterator[Node]: Iterator over all nodes
    """
    queue: deque[Node] = deque([self])
    while queue:
        node = queue.popleft()
        yield node
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)