A powerful, immutable-by-default tree manipulation library for Python with functional programming patterns, composable transformations, and advanced pattern matching.
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"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
pip install AlgoTreefrom AlgoTree import Node, node, Tree, TreeBuilder
# 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"
)
)
# Fluent builder API
tree = (TreeBuilder("root", type="container")
.child("src")
.child("main.py", type="file", size=1024)
.child("utils.py", type="file", size=512)
.up()
.child("docs")
.child("README.md", type="file")
.build())# 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)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))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"))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))")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', ...]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 parentis_leaf- True if node has no childrendepth- Distance from root (root has depth 0)height- Height of subtree rooted at this nodesize- Total number of nodes in subtreepath- 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)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 nodesize- Total number of nodesheight- Height of treeleaves- List of all leaf nodesis_empty- True if tree is empty
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 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")Builders provide fluent APIs for tree construction.
TreeBuilder:
from AlgoTree import TreeBuilder
tree = (TreeBuilder("root", type="container")
.attr(version="1.0")
.child("src", type="directory")
.child("main.py", type="file", size=1024)
.child("utils.py", type="file", size=512)
.up()
.child("tests", type="directory")
.child("test_main.py", type="file")
.build())DSL-style functions:
from AlgoTree import tree, branch, leaf
my_tree = tree("root",
tree("child1",
leaf("grandchild1", value=1),
leaf("grandchild2", value=2)
),
tree("child2",
leaf("grandchild3", value=3)
)
).build()QuickBuilder (path-based):
from AlgoTree import QuickBuilder
tree = (QuickBuilder()
.root("project")
.add("src/main.py", type="file", size=1024)
.add("src/utils.py", type="file", size=512)
.add("tests/test_main.py", type="file")
.add("docs/README.md", type="file")
.build())from AlgoTree import TreeBuilder, name, type_, export_tree
# Build file system tree
fs = (TreeBuilder("home")
.child("user")
.child("documents", type="dir")
.child("report.pdf", type="file", size=1024)
.child("notes.txt", type="file", size=256)
.up()
.child("code", type="dir")
.child("main.py", type="file", size=512)
.child("test.py", type="file", size=128)
.up()
.up()
.build())
# 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)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'):,}")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)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.jsonDecision 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.*"
> exitAlgoTree v2.0 is a complete redesign. Here's how to migrate:
Old v1.x API:
from AlgoTree import TreeBuilder
# v1.x - mutable operations
tree = (TreeBuilder()
.root("company")
.child("engineering")
.child("frontend")
.sibling("backend")
.up()
.sibling("sales")
.build())
# Mutable modification
tree.add_child(Node("marketing"))New v2.0 API:
from AlgoTree import TreeBuilder, Node
# v2.0 - fluent builder
tree = (TreeBuilder("company")
.child("engineering")
.child("frontend")
.child("backend")
.up()
.child("sales")
.build())
# Immutable operations
tree2 = Tree(tree.root.with_child(Node("marketing")))Key changes:
- TreeBuilder now takes root name in constructor, no
.root()method - No ``.sibling()`` method - use
.up()then.child() - 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
Transformers support lazy evaluation for large trees:
# Create lazy pipeline
pipeline = map_(expensive_fn) >> filter_(predicate)
# Only evaluated when called
result = pipeline(tree)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 createdCreate 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)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()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_immutabilityContributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
MIT License - see LICENSE file for details.
- Documentation: https://queelius.github.io/AlgoTree/
- Source Code: https://github.com/queelius/AlgoTree
- Issue Tracker: https://github.com/queelius/AlgoTree/issues
- PyPI: https://pypi.org/project/AlgoTree/
- Changelog: https://github.com/queelius/AlgoTree/blob/master/CHANGELOG.md