pip install graphworksfrom graphworks import Graph, to_text
from graphworks.algorithms import find_path, is_connected
graph = Graph("roads").add_edge("A", "B", weight=2).add_edge("B", "C", label="toll")
print(graph.neighbors("B")) # ['A', 'C'] — undirected edges work both ways
print(find_path(graph, "C", "A")) # ['C', 'B', 'A']
print(is_connected(graph)) # True
print(to_text(graph))Graphs can also be loaded from JSON (Graph(input_file=...) / Graph(input_graph=...)); a
neighbor may be a bare name or an object carrying edge data:
{
"label": "roads",
"directed": false,
"weighted": true,
"vertices": { "A": { "label": "Alpha" } },
"graph": { "A": ["B", { "target": "C", "weight": 2.5, "label": "toll" }], "B": [], "C": [] }
}save_to_json writes this same format, so weights, labels, and attributes round-trip.
Graphis the only class; algorithms (graphworks.algorithms) and exporters (graphworks.export) are free functions that take aGraph.- A vertex name that is not in the graph raises
KeyError(graph[v],neighbors, and every algorithm). Thedict.get-stylegraph.vertex(name)andgraph.edge(u, v)returnNone. - Predicates are vacuously true on an empty graph; metrics that are undefined there
(
min_degree,max_degree,diameter) raiseValueError. - Algorithms whose precondition fails raise
ValueErrorrather than returning a wrong answer (topological_sorton a cyclic graph,find_circuiton a non-Eulerian graph,diameteron a disconnected graph). - Degree counts every incident edge (in + out for directed graphs; a self-loop counts twice).
Optional extras:
pip install graphworks[matrix] # numpy adjacency matrix support
pip install graphworks[viz] # graphviz export
pip install graphworks[docs] # generate documentation- Python 3.13+
- uv (>= 0.10.12)
uv sync --extra all# Run all tests (includes coverage; fails under 90%)
uv run pytest
# Run a single test file
uv run pytest tests/test_graph.py
# Run a single test by name
uv run pytest tests/test_graph.py -k "test_method_name"# Lint
uv run ruff check --fix src/ tests/
# Format
uv run black src/ tests/
uv run isort src/ tests/
# Type checking
uv run ty check
# Code complexity
uv run xenon --max-average=A --max-modules=B --max-absolute=B src/
# Run all pre-commit hooks
prek run --all-filesVersion is managed automatically via git tags using hatchling-vcs.
- Tag a commit:
git tag -a X.Y.Z -m 'release message' - Push the tag:
git push --tags - The GitHub Actions workflow will build and publish to PyPI automatically.
-
Vertexclass with name, optional label, and immutable attribute mapping -
Edgedataclass with source, target, directed flag, optional weight, label, and attributes - Both classes are frozen (immutable) with identity-based equality and hashing
- Internal storage uses
dict[str, Vertex]for vertex lookup - Adjacency structure uses
dict[str, dict[str, Edge]]for O(1) edge access - Edge weights, labels, and attributes survive union, complement (vertices), and JSON/DOT export
-
add_edgesupportsweightandlabelkeyword arguments -
edge()lookup method for direct O(1) edge retrieval -
remove_vertex()/remove_edge()(chainable; removing a vertex drops its incident edges) -
copy()(independent structure, shared immutableVertex/Edgeobjects) -
predecessors()for directed graphs (O(V); same asneighbors()when undirected)
-
to_adjacency_matrix()/from_adjacency_matrix()round-trips that preserve vertex names via an index mapping -
complementpreserves original vertex names and thedirectedflag -
to_edge_list()/from_edge_list()conversions - Weighted JSON format (
{"target": ..., "weight": ...}neighbor objects, seeg4.json)
- Unweighted shortest path via BFS (
find_pathreturns a path, not the shortest;diameteralready computes BFS distances internally) - Dijkstra's shortest path (weighted)
- Prim's / Kruskal's minimum spanning tree
- Strongly connected components (Tarjan or Kosaraju) and
is_strongly_connected
- JSON export (
save_to_json, lossless round-trip) - Graphviz DOT export (
save_to_dot, with vertex/edge labels and weights) - Text tree and 2-D ASCII rendering (
to_text,to_ascii) - Rich-based demo script (
uv run demo) - CLI application for common graph operations
- Rich rendering integration for interactive graph display
- Iterative traversals:
find_path,find_all_paths, andtopological_sortare recursive and raiseRecursionErroron a ~3,000-vertex chain - Input validation hardening:
- Reject non-list neighbor values in JSON (
{"A": "B"}is silently read as the characters of"B") - Raise when more than one of
input_file/input_graph/input_matrixis given (extras are silently ignored)
- Reject non-list neighbor values in JSON (
- Read-only graph view (instead of locking for thread safety)
- Performance benchmarks
- Reverse adjacency index so
predecessors()is O(in-degree) instead of O(V); then switchis_connected,find_circuit, andfind_isolated_verticesto it (they currently rebuild in-edges by hand, which is faster than calling the O(V)predecessors()per vertex). Every add/remove path andcopy()must maintain the index. - Run docstring examples as tests (
--doctest-modules) so examples cannot go stale
-
CHANGELOG.md(linked from the PyPI metadata but missing), starting with the 0.10.0 breaking changes - Hosted API documentation (Sphinx deps exist in the
[docs]extra, but there is nodocs/source and the Read the Docs site does not exist)