NetworkX dict-of-dict adjacency: why flexibility beats raw speed
NetworkX stores adjacency as a dict of dicts to support arbitrary nodes and rich attributes. That flexibility enables incremental building and attribute filtering, with trade-offs in memory and speed at scale.
31 Dec 2025, 09:35 UTC

You need a graph where nodes are routers named by strings, edges carry latency, capacity and last_seen timestamps, and you want to add nodes incrementally as devices come online. A fixed-size adjacency matrix forces integer IDs and a rigid schema. NetworkX’s engineering decision is to store adjacency as a dict mapping node to a dict of neighbor to edge attribute dict. That choice makes arbitrary hashable nodes and rich per-edge data natural, at the cost of memory and raw speed.
The dict-of-dict adjacency is a deliberate flexibility choice
Graph, DiGraph and MultiGraph keep adjacency in G.adj. Conceptually it is node → neighbor → edge attribute dict. Nodes can be strings, tuples, or any hashable Python object. Edge data is a plain dict, not a single weight field.
This model supports incremental construction and mutation without schema changes. You can add a node with attributes, add an edge with attributes, and later filter by those attributes using the Pythonic G.nodes and G.edges views.
import networkx as nx\n\nG = nx.Graph()\nG.add_node('r1', type='router', region='us-east')\nG.add_edge('r1', 'r2', latency_ms=12, capacity_mbps=1000)\n\n# Inspect structure\n# G.adj is a dict: node -> dict of neighbor -> edge attribute dict\n# G.nodes['r1'] is the node attribute dict\n# G.edges['r1','r2'] is the edge attribute dict\nRun this in a Python REPL or script with read access to the environment. No special permissions are required. The practical check is to inspect G.adj after construction and confirm the nested dict shape matches the nodes you added.
Attribute mutation has reference semantics
Node and edge attributes are plain Python dicts. That is convenient for building and filtering, but mutable objects are stored by reference. Reusing the same dict for multiple nodes or edges will couple them unintentionally.
Avoid this pattern:
shared = {'status':'up'}\nG.add_node('a', meta=shared)\nG.add_node('b', meta=shared)\nChanging shared['status'] mutates both nodes. The safe practice is to create a new dict per node or copy before reuse. You can verify isolation by checking id(G.nodes['a']['meta']) versus id(G.nodes['b']['meta']).
Reproducible random graphs via seed
Experiment repeatability is a practical concern. Generators such as barabasi_albert_graph and erdos_renyi_graph accept a seed parameter for the random number generator.
seed = 42\nG1 = nx.barabasi_albert_graph(n=100, m=2, seed=seed)\nG2 = nx.barabasi_albert_graph(n=100, m=2, seed=seed)\nWith the same seed and parameters, the node sets and edge sets are deterministic. A practical check is to compare set(G1.edges()) with set(G2.edges()) in the same process. Different seeds produce different topologies. API signatures for generators are version-sensitive; confirm the signature in your installed NetworkX before relying on keyword names.
Offload numeric work with SciPy interop
Algorithms are implemented in pure Python over the dict adjacency, prioritizing readability and ease of extension over raw performance. For numeric kernels, NetworkX provides conversion helpers.
# Convert to sparse array for linear algebra\nA = nx.to_scipy_sparse_array(G, weight='latency_ms', format='csr')\n\n# Later, build a NetworkX graph from a sparse matrix\nH = nx.from_scipy_sparse_array(A)\nRun conversion in a process with access to SciPy. The expected check is that A.shape[0] equals len(G.nodes) and the matrix is sparse. Keep NetworkX for modeling, I/O and attribute handling, and use SciPy for spectral methods, centrality approximations or solvers that benefit from array formats.
Trade-off: memory and speed at scale
Dict-of-dict adjacency is memory heavy and slower than array-based formats for very large sparse graphs. Performance degrades on million-node scales where Python object overhead dominates. Attribute lookups are fast for small to medium graphs and for irregular access patterns, but bulk numeric sweeps are not.
A practical way to assess fit is to measure memory usage and operation latency on a representative subgraph before scaling. If you need dense linear algebra or GPU acceleration, convert to SciPy or another array backend for the numeric phase and keep NetworkX for the modeling phase.
Use NetworkX when you need flexible node IDs, rich per-edge metadata, incremental mutation and Pythonic attribute filtering. Offload to SciPy for heavy numeric work, and be deliberate about mutable attribute references and generator seed reproducibility.