Skip to main content

AlgoGraph: Immutable Graph Library with Functional Transformers

AlgoGraph is an immutable graph library for Python. Version 2.0.0 introduces pipe-based transformers, declarative selectors, and lazy views, which together cut boilerplate by roughly 90% for common graph operations.

Why Immutability for Graphs?

Mutable graph libraries like NetworkX are powerful but carry hidden costs:

  • Side effects: Modifying a graph can break other code holding references to it
  • Debugging difficulty: Hard to track when and 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()

This is the same idea behind persistent data structures in Clojure or Haskell. You get referential transparency, which means you can reason about graph transformations without worrying about what else might be mutating the same object.

Pipe-Based Transformers

The main 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, ...}

Compare with the imperative alternative:

# 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)

The pipe version reads top to bottom. Each step is a function. You can compose them, reuse them, test them independently.

Available transformers:

  • filter_vertices(pred), filter_edges(pred) – Filter by predicate
  • map_vertices(fn), map_edges(fn) – Transform attributes
  • reverse(), to_undirected() – Structure transformations
  • largest_component(), minimum_spanning_tree() – Algorithm-based
  • to_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)
)

You specify what you want, not how to find it. The selector algebra handles the rest.

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/edges
  • subgraph_view() – View specific vertices
  • reversed_view() – Reverse edge directions
  • undirected_view() – View as undirected
  • neighborhood_view() – k-hop neighborhood

56+ Algorithms

AlgoGraph includes broad 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

I like this shell. Graphs are inherently navigable structures, and treating them like a filesystem makes exploration feel natural.

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

Discussion