AlgoGraph: Immutable Graph Library with Functional Transformers

November 30, 2025

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:

Read More