> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗ whois is redacted, cdns mask the rest — get the real operator — try whoisgeni.us ↗ who really runs that domain ↗ domain attribution that shows its work — full evidence chain — try whoisgeni.us ↗ domain intel w/ evidence ↗

← Back to Articles

NetworkX: Why Python's Most Popular Graph Library Is Built on Dictionaries, Not Arrays

[ View on GitHub ]

NetworkX: Why Python's Most Popular Graph Library Is Built on Dictionaries, Not Arrays

Hook

NetworkX processes a million-edge graph in seconds while igraph does it in milliseconds—yet NetworkX has 10x the adoption. The secret isn't performance; it's that you can install it anywhere Python runs without compiling a single line of C.

Context

Before NetworkX emerged in 2002, Python developers analyzing networks faced an uncomfortable choice: write graph algorithms from scratch or wrestle with C++ libraries through fragile bindings. Academic researchers studying social networks needed to implement Dijkstra's algorithm for the hundredth time. Data scientists exploring citation graphs had to choose between MATLAB's expensive licenses or Boost Graph Library's template metaprogramming nightmares.

NetworkX solved a different problem than raw performance: it made graph theory accessible. Co-created by mathematicians at Los Alamos National Laboratory, it prioritized algorithm breadth and API clarity over speed. The library became the NumPy of graph analysis—not the fastest, but the first tool you reach for because it works everywhere Python does. With 17,000+ GitHub stars and dependencies from scikit-learn to COVID-19 contact tracing research, NetworkX won by being the path of least resistance for tens of thousands of developers who need graph algorithms more than they need microsecond execution times.

Technical Insight

Visualization

Numerical Backend

Algorithm Layer

Core Graph Classes

create/modify

stores in

call algorithms

read graph via

dict lookups

matrix ops

draw graph

reads

renders via

User Code

Graph/DiGraph

MultiGraph/MultiDiGraph

Nested Dict Storage

_adj/_node/_pred

Pure Functions

shortest_path, clustering, etc

Duck-Type Dispatch

graph method checks

NumPy/SciPy

matrix operations

Layout Algorithms

spring, circular, etc

Matplotlib Wrapper

System architecture — auto-generated

NetworkX's architecture is deceptively simple: graphs are nested Python dictionaries. The core implementation uses three-level nesting where G._adj[u][v] stores edge attributes for the edge from node u to node v. This dict-of-dicts design means every data structure operation is a native Python dict lookup—no ctypes bridges, no memory marshaling, no compilation.

Here's what makes this interesting. Create a graph and inspect its internals:

import networkx as nx

G = nx.DiGraph()
G.add_edge('Alice', 'Bob', weight=0.8, relationship='friend')
G.add_edge('Bob', 'Charlie', weight=0.3, relationship='colleague')

# The internal representation is pure Python dicts
print(G._adj)
# {'Alice': {'Bob': {'weight': 0.8, 'relationship': 'friend'}},
#  'Bob': {'Charlie': {'weight': 0.3, 'relationship': 'colleague'}},
#  'Charlie': {}}

# Edge lookup is two dictionary accesses
print(G._adj['Alice']['Bob']['weight'])  # 0.8

# Algorithms work as pure functions, not methods
path = nx.shortest_path(G, 'Alice', 'Charlie', weight='weight')
print(path)  # ['Alice', 'Bob', 'Charlie']

This dict-based approach creates fascinating trade-offs. Edge existence checks are O(1) average case—checking if (u,v) exists is just v in G._adj[u]. Sparse graphs consume memory proportional to edges, not nodes². But every edge access requires two hash table lookups and pointer dereferences. In tight loops processing millions of edges, this overhead dominates. An array-based graph representation would store edges contiguously, enabling CPU cache locality and SIMD vectorization. NetworkX's dicts scatter memory across the heap.

The algorithmic layer reveals why this works. NetworkX implements over 200 graph algorithms as standalone functions, not class methods. This functional design means nx.betweenness_centrality(G) can work on any object that implements the graph protocol—whether it's a NetworkX Graph, a custom subclass, or even a NumPy adjacency matrix wrapped in a compatible interface. The library uses duck typing extensively:

# You can implement a custom graph and NetworkX algorithms just work
class CustomGraph:
    def __init__(self):
        self._edges = {}
    
    def __iter__(self):
        # NetworkX algorithms iterate over nodes
        return iter(self._edges.keys())
    
    def __getitem__(self, node):
        # This makes G[node] return neighbors
        return self._edges.get(node, {})
    
    # Many algorithms work with just these methods
    def nodes(self):
        return self._edges.keys()

# Custom implementations can plug into the ecosystem
G_custom = CustomGraph()
# nx.algorithm(G_custom) works if the protocol matches

The library uses lazy evaluation for graph views. When you call G.nodes() or G.edges(), you don't get a list—you get a view object that references the underlying dict. This means zero-copy iteration for read operations, but creates mutation hazards:

G = nx.Graph([(1, 2), (2, 3), (3, 4)])

# This returns a view, not a copy
nodes = G.nodes()
print(type(nodes))  # <class 'networkx.classes.reportviews.NodeView'>

# Dangerous: modifying the graph during iteration
for node in G.nodes():
    if node % 2 == 0:
        G.remove_node(node)  # RuntimeError: dictionary changed size

# Safe: materialize to list first
for node in list(G.nodes()):
    if node % 2 == 0:
        G.remove_node(node)  # Works

NetworkX's performance model is two-tiered. Pure graph traversal (BFS, DFS, shortest paths on unweighted graphs) runs at Python speed—slow but predictable. Algorithms requiring numerical computation dispatch to NumPy/SciPy. Spectral clustering converts the graph to a SciPy sparse matrix, computes eigenvalues with LAPACK, then converts back. This hybrid approach means some algorithms are surprisingly fast while others crawl.

The recent backend system adds a plugin architecture. Decorate algorithms with @nx._dispatch and they can be transparently routed to alternative implementations like nx-cugraph (GPU) or nx-parallel (multiprocessing) without changing user code. The challenge? Most graph algorithms are inherently sequential or have limited parallelism. BFS can't effectively parallelize. Dijkstra's algorithm has dependencies between iterations. Only embarrassingly parallel workloads like computing centrality for independent nodes benefit.

Gotcha

NetworkX's performance ceiling is brutally low for production systems. The dict-of-dicts overhead means you're paying 200+ bytes per edge in memory. A social network graph with 1 million edges consumes multiple gigabytes where igraph uses tens of megabytes. Traversal speed is 10-100x slower than C-backed alternatives—not because the algorithms are wrong, but because Python's interpreter overhead dominates. Running PageRank on a 100K-node citation network takes minutes in NetworkX versus seconds in graph-tool. This isn't a constant-factor slowdown; it compounds with graph size. Once you exceed 100K edges, you'll spend more time waiting for NetworkX than writing code.

The attribute system is dangerously permissive. Nodes can be any hashable object—integers, strings, tuples, even custom classes. Edge attributes are arbitrary dicts with zero validation. You can accidentally create graphs where node IDs are inconsistent types (mixing integers and strings), or edge attributes are malformed, and NetworkX won't complain until an algorithm deep in execution tries to sort nodes or sum weights. Debugging these failures is painful because the error happens far from where you created the malformed data. The library's philosophy is 'consenting adults'—it trusts you to maintain invariants—but this makes it easy to shoot yourself in the foot at scale.

Verdict

Use NetworkX if: you're prototyping network analysis in Jupyter notebooks, teaching graph theory concepts where readable code matters more than speed, or working in restricted environments (AWS Lambda, HPC clusters with no build tools) where installing C dependencies is impossible. It's the right choice for researchers implementing new algorithms who want to focus on logic rather than memory management, or data scientists exploring graphs under 50K edges where the performance hit doesn't matter. The comprehensive algorithm library (200+ implementations) means you'll find shortest paths, centrality measures, community detection, and matching algorithms already written. Skip NetworkX if: you're building production systems processing graphs over 100K edges, need sub-second query responses, or require advanced graph types like temporal networks or property graphs. Choose igraph or graph-tool instead—the 100x performance gap isn't marketing, it's polynomial scaling reality. Also skip it if you're working with dense graphs or need real-time visualization; the memory overhead and O(n²) layout algorithms make it unsuitable for interactive applications. If your graph fits in memory and your computation budget is measured in minutes rather than milliseconds, NetworkX's zero-friction installation and massive algorithm library win. Otherwise, invest the time to compile something faster.