A Production Graph Adjacency List With Node Metadata

The bare adjacency list in the official catalog is a starting point. In production I need node metadata, safe edge deletion, and a traversal helper that does not break when a node is removed mid-walk.

Python
Compiler
3 snippets
adjacency-list
graph-representation
py-collections
kavyachakraborty

By @kavyachakraborty

December 20, 2025

·

Updated May 20, 2026

706 views

5

4.2 (10)

from __future__ import annotations
from collections import defaultdict
from typing import Any, Iterable

# A real graph almost always carries metadata: a node has a label, a created_at,
# maybe a status. An edge has a relationship type. Plain { node: [neighbors] }
# loses both. Two parallel structures keep them.

class Graph:
    def __init__(self) -> None:
        self.nodes: dict[str, dict] = {}
        self.edges: dict[str, dict[str, str]] = defaultdict(dict)  # src -> {dst: rel}

    def add_node(self, node_id: str, **data: Any) -> None:
        self.nodes[node_id] = data

    def add_edge(self, src: str, dst: str, rel: str = 'links_to') -> None:
        if src not in self.nodes or dst not in self.nodes:
            raise KeyError(f'unknown node: {src!r} or {dst!r}')
        self.edges[src][dst] = rel

    def neighbors(self, node_id: str) -> Iterable[tuple[str, str]]:
        return list(self.edges.get(node_id, {}).items())

g = Graph()
g.add_node('article:1', title='How I sized my JVM heap', author='alice')
g.add_node('user:alice', name='Alice', joined='2024-01-15')
g.add_node('article:2', title='Tracing in 30 lines', author='alice')
g.add_edge('user:alice', 'article:1', 'authored')
g.add_edge('user:alice', 'article:2', 'authored')
g.add_edge('article:1', 'article:2', 'see_also')

print(g.nodes['article:1'])
for dst, rel in g.neighbors('user:alice'):
    print(f'user:alice --{rel}--> {dst}  ({g.nodes[dst]["title"]})')

Two stores, one purpose: nodes holds the metadata dict per id, and edges holds a src -> {dst: rel} map so each edge has a typed relationship. The cost of the metadata dict is one extra hash lookup per node access, which is negligible compared to having to denormalize node data into every edge. The add_edge checks both endpoints; that single line has caught dozens of typos in production. The neighbors method returns a list (not the underlying dict view) so the caller can safely mutate the graph during iteration, which the next accordion needs.