A powerful, immutable-by-default tree manipulation library for Python with functional programming patterns, composable transformations, and advanced pattern matching.
⚠️ BREAKING CHANGES in v2.0.0 ⚠️
Version 2.0.0 is a complete redesign with NO backward compatibility.
Immutable-by-default API - All operations return new trees
Functional programming style - map, filter, reduce, fold operations
Composable transformations - Selectors and transformers compose with operators
Type-safe with full type hints - Complete IDE support
86% test coverage - 197 passing tests across comprehensive test suite
Upgrading from v1.x:
The v1.x API has been completely removed. See the Migration Guide below.
If you need the old API:
pip install "AlgoTree<2.0.0"
Introduction
AlgoTree v2.0 provides a modern, functional approach to working with tree structures in Python. Built on immutable-by-default principles, it offers:
Immutable trees - Safe, predictable transformations without side effects
Functional operations - map, filter, reduce, fold, and more
Pattern matching - Powerful selectors with wildcards and composition
Composable transformers - Build complex pipelines with simple operators
Multiple construction styles - Fluent builders, DSL parsing, factory methods
Rich export formats - JSON, GraphViz, Mermaid, paths, and more
Interactive shell - Terminal-based exploration and quick operations
For Scripting & Automation: Use the fluent Python API (recommended)
For Interactive Exploration: Use the shell/CLI tools
Installation
pip install AlgoTree
Quick Start
Building Trees
from AlgoTree import Node, node
# Simple construction with Node
tree = Node("root",
Node("child1", attrs={"value": 1}),
Node("child2", attrs={"value": 2})
)
# Convenience function (auto-converts strings)
tree = node("root",
node("child1", value=1),
"child2", # Strings auto-convert to nodes
node("child3",
"grandchild1",
"grandchild2"
)
)
# From paths
tree = Node.from_paths([
"root/src/main.py",
"root/src/utils.py",
"root/docs/README.md",
])
Immutable Transformations
# All operations return new trees
tree2 = tree.with_name("new_root")
tree3 = tree.with_attrs(status="active")
tree4 = tree.with_child(Node("new_child"))
# Functional tree-wide operations
doubled = tree.map(lambda n: n.with_attrs(
value=n.get("value", 0) * 2
))
filtered = tree.filter(lambda n: n.get("value", 0) > 5)
nodes = tree.find_all(lambda n: n.is_leaf)
Composable Selectors
from AlgoTree import name, attrs, leaf, type_
# Pattern matching with wildcards
selector = name("*.txt")
# Attribute matching with predicates
selector = attrs(size=lambda s: s > 1000)
# Logical composition with operators
selector = type_("file") & ~leaf()
# Structural selectors
selector = type_("file").child_of(name("src"))
selector = leaf().at_depth(2)
# Use selectors with trees
matching_nodes = list(selector.select(tree))
Composable Transformers
from AlgoTree import map_, filter_, prune, normalize, extract
# Build transformation pipelines with >> operator
pipeline = (
map_(lambda n: {"processed": True}) >>
filter_(lambda n: n.get("active")) >>
normalize(sort_children=True) >>
extract(lambda n: n.name)
)
# Apply to tree
result = pipeline(tree)
# Or use Tree's fluent API
result = (Tree(tree)
.map(lambda n: {"processed": True})
.filter(lambda n: n.get("active"))
.prune(lambda n: n.name == "temp"))
DSL Parsing
from AlgoTree import parse_tree
# Visual ASCII format
tree = parse_tree("""
root
├── child1
│ ├── grandchild1
│ └── grandchild2
└── child2
""")
# Indented format
tree = parse_tree("""
root
child1
grandchild1
grandchild2
child2
""")
# S-expression format
tree = parse_tree("(root (child1 (grandchild1) (grandchild2)) (child2))")
Export & Serialization
from AlgoTree import export_tree, save, load
# Export to various formats
json_str = export_tree(tree, "json")
dot_str = export_tree(tree, "graphviz")
mermaid_str = export_tree(tree, "mermaid")
# File operations
save(tree, "tree.json")
loaded = load("tree.json")
# Export to dictionary
data = Tree(tree).to_dict()
# Export to paths
paths = Tree(tree).to_paths() # ['root/child1', 'root/child2', ...]
Core API
Node (Immutable)
The Node class is the foundation of AlgoTree v2.0. It’s immutable by default, making trees safe to share and transform.
Properties:
name - Node name (immutable)
attrs - Node attributes dictionary (returns copy)
children - Tuple of child nodes (immutable)
parent - Parent node (weak reference)
is_root - True if node has no parent
is_leaf - True if node has no children
depth - Distance from root (root has depth 0)
height - Height of subtree rooted at this node
size - Total number of nodes in subtree
path - Path from root as string (e.g., “root/child/grandchild”)
Immutable transformations:
new_node = node.with_name("new_name")
new_node = node.with_attrs(key="value")
new_node = node.without_attrs("key1", "key2")
new_node = node.with_child(Node("child"))
new_node = node.with_children(Node("a"), Node("b"))
new_node = node.without_child("child_name")
new_node = node.map_children(lambda c: c.with_attrs(tagged=True))
new_node = node.filter_children(lambda c: c.get("active"))
Tree-wide transformations:
new_tree = node.map(lambda n: n.with_attrs(processed=True))
filtered = node.filter(lambda n: n.get("value") > 0)
found = node.find(lambda n: n.name == "target")
all_matches = node.find_all(lambda n: n.is_leaf)
Iteration:
for n in node.walk("preorder"): # or "postorder", "levelorder"
print(n.name)
for descendant in node.descendants():
print(descendant.name)
for ancestor in node.ancestors():
print(ancestor.name)
for leaf in node.leaves():
print(leaf.name)
Tree (Wrapper)
The Tree class wraps a root node and provides a consistent fluent API.
Factory methods:
tree = Tree.from_dict({
"name": "root",
"value": 1,
"children": [
{"name": "child1", "value": 2},
{"name": "child2", "value": 3}
]
})
tree = Tree.from_paths([
"root/a/b",
"root/a/c",
"root/d"
])
Functional operations:
# Map over all nodes
result = tree.map(lambda n: {"processed": True})
# Filter nodes (preserves structure)
result = tree.filter(lambda n: n.get("active"))
# Reduce to single value
total = tree.reduce(
lambda acc, n: acc + n.get("value", 0),
0
)
# Fold bottom-up
result = tree.fold(
lambda node, child_results: sum(child_results) + 1
)
Structure operations:
pruned = tree.prune(lambda n: n.name == "temp")
grafted = tree.graft(lambda n: n.is_leaf, Node("new_child"))
flattened = tree.flatten(max_depth=2)
Query operations:
node = tree.find(lambda n: n.name == "target")
nodes = tree.find_all(lambda n: n.is_leaf)
exists = tree.exists(lambda n: n.get("error"))
count = tree.count(lambda n: n.get("active"))
Properties:
root - Root node
size - Total number of nodes
height - Height of tree
leaves - List of all leaf nodes
is_empty - True if tree is empty
Selectors
Selectors provide composable pattern matching for tree nodes.
Basic selectors:
from AlgoTree import name, attrs, type_, predicate, depth, leaf, root
# Name matching with wildcards
sel = name("*.txt")
sel = name("file_*")
# Attribute matching
sel = attrs(type="file")
sel = attrs(size=lambda s: s > 1000) # With predicate
sel = attrs(type="file", active=True) # Multiple attrs
# Type matching
sel = type_("directory")
# Custom predicate
sel = predicate(lambda n: n.name.startswith("test_"))
# Depth matching
sel = depth(2) # Nodes at depth 2
# Leaf/root selectors
sel = leaf()
sel = root()
Logical composition:
# AND
sel = name("*.py") & type_("file")
# OR
sel = name("*.py") | name("*.txt")
# NOT
sel = ~leaf()
# XOR
sel = type_("file") ^ leaf()
Structural composition:
# Child of
sel = name("*.py").child_of(name("src"))
# Parent of
sel = type_("directory").parent_of(name("main.py"))
# Descendant of
sel = name("*.txt").descendant_of(name("docs"))
# Ancestor of
sel = type_("directory").ancestor_of(leaf())
# Sibling of
sel = name("config.json").sibling_of(name("main.py"))
# At specific depth
sel = type_("file").at_depth(3)
Using selectors:
# Select all matching nodes
for node in selector.select(tree):
print(node.name)
# Get first match
first = selector.first(tree)
# Count matches
count = selector.count(tree)
# Check existence
exists = selector.exists(tree)
Transformers
Transformers provide composable tree transformations.
Tree -> Tree transformers:
from AlgoTree import map_, filter_, prune, graft, flatten, normalize, annotate
# Map over nodes
t = map_(lambda n: {"processed": True})
t = map_(lambda n: n.with_attrs(value=n.get("value", 0) * 2))
# Filter nodes
t = filter_(lambda n: n.get("active"))
t = filter_(name("*.txt")) # Can use selectors
# Prune (remove) nodes
t = prune(lambda n: n.name == "temp")
t = prune(name("test_*"))
# Graft (add) subtrees
t = graft(leaf(), Node("new_child"))
# Flatten to depth
t = flatten(max_depth=2)
# Normalize (sort, deduplicate)
t = normalize(sort_children=True, dedup=True)
# Annotate (add metadata)
t = annotate(lambda n: {"depth": n.depth, "size": n.size})
Tree -> Any transformers (shapers):
from AlgoTree import reduce_, fold, extract, to_dict, to_paths
# Reduce to single value
t = reduce_(lambda acc, n: acc + n.get("value", 0), initial=0)
# Fold bottom-up
t = fold(lambda node, child_results: sum(child_results) + 1)
# Extract values
t = extract(lambda n: n.name)
# Convert to dictionary
t = to_dict(children_key="children")
# Convert to paths
t = to_paths(to_leaves_only=True)
Composition:
# Sequential composition with >>
pipeline = map_(fn1) >> filter_(pred) >> prune(sel)
# Parallel composition with &
both = map_(fn1) & annotate(fn2)
# Conditional application
conditional = map_(fn).when(lambda t: t.size > 10)
# Repeated application
repeated = normalize().repeat(3)
# Debug intermediate results
debug = pipeline.debug("after_filter")
Tree Construction
AlgoTree provides several ways to construct trees (the builders.py module was removed in v2.0; use these approaches instead).
Direct construction:
from AlgoTree import Node, node
tree = Node("root", attrs={"type": "container", "version": "1.0"},
*(Node(name, attrs={"type": "directory"})
for name in ("src", "tests")))
# Or with the node() convenience function
my_tree = node("root",
node("child1",
node("grandchild1", value=1),
node("grandchild2", value=2)
),
node("child2",
node("grandchild3", value=3)
)
)
Path-based construction:
from AlgoTree import Node
tree = Node.from_paths([
"project/src/main.py",
"project/src/utils.py",
"project/tests/test_main.py",
"project/docs/README.md",
])
From a dict:
from AlgoTree import Node
tree = Node.from_dict({
"name": "root",
"type": "container",
"children": [
{"name": "src", "children": [
{"name": "main.py", "type": "file", "size": 1024},
{"name": "utils.py", "type": "file", "size": 512}
]},
{"name": "tests", "children": [
{"name": "test_main.py", "type": "file"}
]}
]
})
Examples
File System Tree
from AlgoTree import Node, node, name, export_tree
# Build file system tree
fs = node("home",
node("user",
node("documents", type="dir",
*[node("report.pdf", type="file", size=1024),
node("notes.txt", type="file", size=256)]
),
node("code", type="dir",
*[node("main.py", type="file", size=512),
node("test.py", type="file", size=128)]
)
)
)
# Find all Python files
py_files = fs.find_all(name("*.py"))
for f in py_files:
print(f.path, f.get("size"))
# Calculate total size of all files
total_size = fs.reduce(
lambda acc, n: acc + n.get("size", 0),
0
)
print(f"Total size: {total_size} bytes")
# Export to GraphViz
dot = export_tree(fs, "graphviz")
print(dot)
Organization Hierarchy
from AlgoTree import node, attrs, map_, normalize
# Build organization tree
company = node("TechCorp",
node("Engineering",
node("Frontend", team_size=10, budget=500000),
node("Backend", team_size=15, budget=750000),
node("DevOps", team_size=5, budget=300000)
),
node("Sales",
node("Enterprise", team_size=8, budget=400000),
node("SMB", team_size=12, budget=350000)
),
node("Marketing",
node("Digital", team_size=6, budget=250000),
node("Content", team_size=4, budget=150000)
)
)
# Calculate department budgets
result = company.map(lambda n: {
"total_budget": sum(
child.get("budget", 0) for child in n.children
) if n.children else n.get("budget", 0)
})
# Find high-budget teams
high_budget = result.find_all(
lambda n: n.get("budget", 0) > 400000
)
for team in high_budget:
print(f"{team.path}: ${team.get('budget'):,}")
Data Processing Pipeline
from AlgoTree import Tree, map_, filter_, normalize, extract
# Create data tree
data = Tree.from_dict({
"name": "dataset",
"children": [
{"name": "record_1", "value": 10, "valid": True},
{"name": "record_2", "value": 5, "valid": False},
{"name": "record_3", "value": 15, "valid": True},
{"name": "record_4", "value": 8, "valid": True},
]
})
# Build processing pipeline
pipeline = (
filter_(lambda n: n.get("valid", False)) >>
map_(lambda n: {"value": n.get("value", 0) * 2}) >>
normalize(sort_children=True) >>
extract(lambda n: n.get("value"))
)
# Process data
result = pipeline(data)
print("Processed values:", result)
When to Use Python API vs Shell
AlgoTree provides two interfaces for different use cases:
Use the Python API (Recommended for Scripting):
from AlgoTree import Node, Tree, node
from AlgoTree.transformers import map_, filter_, prune
# Fluent, composable, type-safe
tree = Tree(node('root',
node('child1', value=1),
node('child2', value=2)
))
# Powerful transformations with full Python
result = tree.map(lambda n: n.with_attrs(doubled=n.get('value', 0) * 2))
filtered = tree.filter(lambda n: n.get('value', 0) > 1)
# Integration with Python ecosystem
import pandas as pd
df = pd.DataFrame([{'name': n.name, 'value': n.get('value')}
for n in tree.root.descendants()])
Use the Shell/CLI (For Interactive Exploration):
# Quick exploration
algotree shell tree.json
# In the shell:
> cd root
> ls
> find "child.*"
> select "n.get('value', 0) > 1"
> tree
# One-off terminal operations
algotree tree tree.json
algotree select 'n.depth > 2' tree.json
Decision Matrix:
Use Case |
Python API |
Shell/CLI |
|---|---|---|
Automation |
Recommended |
Not ideal |
Scripts/Programs |
Recommended |
Not ideal |
Complex logic |
Recommended |
Not ideal |
Type safety |
Recommended |
No type checking |
IDE support |
Recommended |
N/A |
Testing |
Recommended |
Limited |
Integration |
Recommended |
Limited |
Quick exploration |
Possible |
Recommended |
Terminal workflows |
Possible |
Recommended |
Learning structure |
Possible |
Recommended |
Human interaction |
Possible |
Recommended |
Example: Automation with Python API
# Production-ready script
from AlgoTree import Tree, map_, filter_, save
# Load, transform, save
tree = Tree.from_dict(load_data())
processed = tree.filter(validate).map(enrich).prune(cleanup)
save(processed.root, 'output.json')
# Full error handling, logging, etc.
Example: Quick Exploration with Shell
# Quick terminal session
$ algotree shell data.json
> tree
> cd interesting_node
> stat
> find "pattern.*"
> exit
Migration from v1.x
AlgoTree v2.0 is a complete redesign. Here’s how to migrate:
Old v1.x API:
# v1.x - mutable operations (no longer works in v2.0+)
# TreeBuilder, FlatForest, FlatForestNode, TreeNode are all removed.
New v2.0 API:
from AlgoTree import node, Node, Tree
# v2.0 - functional construction
tree = node("company",
node("engineering",
node("frontend"),
node("backend")
),
node("sales")
)
# Immutable operations
tree2 = tree.with_child(Node("marketing"))
Key changes:
TreeBuilder removed - use node(), Node(), Node.from_dict(), Node.from_paths(), or parse_tree()
All operations are immutable - return new trees instead of mutating
Node is immutable - use .with_*() methods instead of property setters
No FlatForest, FlatForestNode, TreeNode - removed entirely
Tree wrapper provides functional API (map, filter, reduce, fold)
Selectors replace string-based pattern matching
Transformers provide composable pipelines
Advanced Features
Lazy Evaluation
Transformers support lazy evaluation for large trees:
# Create lazy pipeline
pipeline = map_(expensive_fn) >> filter_(predicate)
# Only evaluated when called
result = pipeline(tree)
Structural Sharing
Immutable operations use structural sharing for efficiency:
tree1 = Node("root", Node("a"), Node("b"))
tree2 = tree1.with_child(Node("c"))
# tree1 and tree2 share "a" and "b" nodes
# Only "c" and new root are created
Custom Selectors
Create custom selectors by subclassing:
from AlgoTree import Selector
class ExtensionSelector(Selector):
def __init__(self, ext):
self.ext = ext
def matches(self, node):
return node.name.endswith(self.ext)
# Use it
py_files = ExtensionSelector(".py")
for node in py_files.select(tree):
print(node.name)
Custom Transformers
Create custom transformers:
from AlgoTree import TreeTransformer
class CapitalizeNames(TreeTransformer):
def __call__(self, tree):
return tree.map(lambda n: n.with_name(n.name.upper()))
# Use it
uppercase_tree = CapitalizeNames()(tree)
# Or compose it
pipeline = CapitalizeNames() >> normalize()
Testing
AlgoTree v2.0 has 86% test coverage with 197 passing tests:
# Run tests
python -m pytest
# Run with coverage
python -m pytest --cov=AlgoTree --cov-report=html
# Run specific test
python -m pytest test/test_node.py::TestNode::test_immutability
Contributing
Contributions are welcome! Please:
Fork the repository
Create a feature branch
Add tests for new functionality
Ensure all tests pass
Submit a pull request
License
MIT License - see LICENSE file for details.
Links
Documentation: https://queelius.github.io/AlgoTree/
Source Code: https://github.com/queelius/AlgoTree
Issue Tracker: https://github.com/queelius/AlgoTree/issues
Changelog: https://github.com/queelius/AlgoTree/blob/master/CHANGELOG.md
Release files for AlgoTree 3.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| algotree-3.0.0.tar.gz | 165.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| algotree-3.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 276.1 kB
Release files / algotree-3.0.0.tar.gz
| Download URL | algotree-3.0.0.tar.gz |
|---|---|
| Size | 165.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4a9136ee4a898db8ff718bff3802a2cced571d0d49e6961a11b63909f948116f
|
|
BLAKE2b-256 checksum How to use checksums |
cfeada2028edbea8163a2748321eba3b9565d9a0230f4d17777ff4f5e1fbe8db
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.20
|
Release files / algotree-3.0.0-py3-none-any.whl
| Download URL | algotree-3.0.0-py3-none-any.whl |
|---|---|
| Size | 110.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d9193396c394ce1993e2a3ebc9492efd75d26e375cb2c0cbcea4b3510a820cb1
|
|
BLAKE2b-256 checksum How to use checksums |
a5fe2bb18a64bb84a5cb3e8928c0066980b6788ab9b28ba06d8dc9b580dcfdbf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.20
|