AlgoGraph is an immutable graph library for Python that brings functional programming elegance to graph algorithms. Version 2.0.0 introduces pipe-based transformers, declarative selectors, and lazy views—achieving ~90% code reduction for common operations.
Why Immutability for Graphs?
Mutable graph libraries like NetworkX are powerful but come with hidden costs:
- Side effects: Modifying a graph can break other code holding references
- Debugging difficulty: Hard to track when/where a graph changed
- Thread unsafety: Concurrent modifications cause subtle bugs
AlgoGraph takes a different approach: all operations return new graph objects. The original is never modified.
from AlgoGraph import Graph
g1 = Graph.from_edges(('A', 'B'), ('B', 'C'))
g2 = g1.add_vertex('D') # g1 unchanged, g2 is new graph
assert 'D' not in g1.vertices()
assert 'D' in g2.vertices()
Pipe-Based Transformers
The killer feature of v2.0.0 is the transformer pipeline using Python’s | operator:
from AlgoGraph.transformers import filter_vertices, largest_component, stats
# Compose operations declaratively
result = (graph
| filter_vertices(lambda v: v.get('active'))
| largest_component()
| stats())
# result: {'vertex_count': 42, 'edge_count': 156, 'density': 0.18, ...}
This replaces verbose imperative code:
# Old way (NetworkX-style)
active = graph.subgraph([v for v in graph.vertices() if v.attrs.get('active')])
components = list(connected_components(active))
largest = max(components, key=len)
subgraph = active.subgraph(largest)
stats = compute_stats(subgraph)
Available transformers:
filter_vertices(pred),filter_edges(pred)— Filter by predicatemap_vertices(fn),map_edges(fn)— Transform attributesreverse(),to_undirected()— Structure transformationslargest_component(),minimum_spanning_tree()— Algorithm-basedto_dict(),to_adjacency_list(),stats()— Export operations
Declarative Selectors
Query vertices and edges with logical operators instead of filtering lambdas:
from AlgoGraph.graph_selectors import vertex as v, edge as e
# Find active users with high degree
power_users = graph.select_vertices(
v.attrs(active=True) & v.degree(min_degree=10)
)
# Find heavy edges from admin nodes
admin_edges = graph.select_edges(
e.source(v.attrs(role='admin')) & e.weight(min_weight=100)
)
# Complex queries with OR, NOT, XOR
special = graph.select_vertices(
(v.attrs(vip=True) | v.degree(min_degree=50)) & ~v.attrs(banned=True)
)
This is declarative: you specify what you want, not how to find it.
Lazy Views
Views provide efficient filtering without copying data:
from AlgoGraph.views import filtered_view, neighborhood_view
# Create view without copying (O(1) space)
view = filtered_view(
large_graph,
vertex_filter=lambda v: v.get('active'),
edge_filter=lambda e: e.weight > 5.0
)
# Iterate lazily
for vertex in view.vertices():
process(vertex)
# Materialize only when needed
small_graph = view.materialize()
# Explore k-hop neighborhood
local = neighborhood_view(graph, center='Alice', k=2)
View types:
filtered_view()— Filter vertices/edgessubgraph_view()— View specific verticesreversed_view()— Reverse edge directionsundirected_view()— View as undirectedneighborhood_view()— k-hop neighborhood
56+ Algorithms
AlgoGraph includes comprehensive algorithm coverage:
Traversal
from AlgoGraph.algorithms import dfs, bfs, topological_sort, find_path
visited = dfs(graph, start_vertex='A')
levels = bfs(graph, start_vertex='A')
order = topological_sort(graph) # DAG only
path = find_path(graph, 'A', 'Z')
Shortest Paths
from AlgoGraph.algorithms import dijkstra, bellman_ford, floyd_warshall, a_star
distances = dijkstra(graph, source='A')
distances = bellman_ford(graph, source='A') # Handles negative weights
all_pairs = floyd_warshall(graph)
path = a_star(graph, 'A', 'Z', heuristic=h)
Centrality
from AlgoGraph.algorithms import pagerank, betweenness_centrality, closeness_centrality
pr = pagerank(social_network)
bc = betweenness_centrality(network)
cc = closeness_centrality(network)
Flow Networks
from AlgoGraph.algorithms import max_flow, min_cut
flow_value = max_flow(network, 'Source', 'Sink')
cut_value, source_set, sink_set = min_cut(network, 'Source', 'Sink')
Matching
from AlgoGraph.algorithms import hopcroft_karp, maximum_bipartite_matching
matching = hopcroft_karp(bipartite_graph, left_set, right_set)
max_matching = maximum_bipartite_matching(graph, left, right)
Graph Coloring
from AlgoGraph.algorithms import welsh_powell, chromatic_number, dsatur
coloring = welsh_powell(graph)
num_colors = chromatic_number(graph)
coloring = dsatur(graph) # Often better than greedy
Interactive Shell
Explore graphs with a filesystem-like interface:
pip install AlgoGraph
algograph network.json
graph(5v):/$ ls
Alice/ [2 neighbors]
Bob/ [3 neighbors]
graph(5v):/$ cd Alice
graph(5v):/Alice$ ls
Attributes:
age = 30
neighbors/ [2 vertices]
graph(5v):/Alice$ path Bob Eve
Path found: Alice -> Bob -> Diana -> Eve
Relationship to AlgoTree
AlgoGraph and AlgoTree share the same design philosophy:
- Immutable data structures
- Pipe-based transformers
- Declarative selectors
- Lazy views
They also interoperate:
from AlgoTree import Tree
from AlgoGraph import tree_to_graph, graph_to_tree
# Tree to Graph
graph = tree_to_graph(tree)
# Graph to Tree (extracts spanning tree)
tree = graph_to_tree(graph, root='root')
Quick Start
pip install AlgoGraph
from AlgoGraph import Graph
from AlgoGraph.algorithms import dijkstra, pagerank
from AlgoGraph.transformers import filter_vertices, stats
# Build a graph
g = (Graph.builder()
.add_vertex('A', age=30)
.add_vertex('B', age=25)
.add_edge('A', 'B', weight=5.0)
.build())
# Run algorithms
distances = dijkstra(g, source='A')
pr = pagerank(g)
# Transform with pipes
result = g | filter_vertices(lambda v: v.get('age') > 20) | stats()
Resources
- PyPI: pypi.org/project/AlgoGraph/
- GitHub: github.com/queelius/AlgoGraph
- Documentation: queelius.github.io/AlgoGraph/
AlgoGraph: Functional programming meets graph algorithms. Immutable, composable, and elegant.
Discussion