networkx
NetworkX is a pure-Python library for building and analysing graphs. A graph is a dict of dicts under the hood, so any hashable Python object can be a node, every node and edge carries an arbitrary attribute dictionary, and you build structures by calling add_edge rather than by declaring a schema. On top of that sit several hundred implemented algorithms: shortest paths, centrality measures, community detection, flow, matching, clique finding, isomorphism, traversal, and generators for classic graph families. It converts to and from pandas DataFrames, SciPy sparse arrays, and NumPy matrices, reads and writes GraphML, GEXF, edge lists, and JSON, and since 3.x it can dispatch algorithm calls to compiled backends such as GPU or parallel implementations without changing your code.
The default graph library in Python and the right first choice for analysis, prototyping, and anything under roughly a hundred thousand edges, because no other library has this algorithm coverage or this gentle an API. Move to rustworkx or igraph when the graph outgrows the interpreter, and keep NetworkX around for the algorithms they do not have.
Use it if
- You are exploring a graph rather than serving one: notebooks, one-off analyses, and research code where the breadth of ready algorithms saves you more time than raw speed would
- Your nodes are real Python objects with attributes: strings, tuples, dataclasses, and per-edge dictionaries all work directly, with no id mapping layer to maintain
- You need an algorithm that only exists here: the coverage of community detection, flow, matching, and isomorphism variants is wider than any compiled alternative
- You want an easy install with no compiler: the base package has zero required dependencies and works anywhere Python runs, including restricted build environments
- You are writing something teachable or reviewable, where reading nx.betweenness_centrality in the diff communicates intent better than a hand-rolled traversal
- Your graph has millions of edges: storage is nested Python dictionaries, so every node and edge costs full Python object overhead, and algorithms run in interpreted loops. rustworkx and igraph implement the same algorithms in compiled code and will fit graphs in memory that NetworkX cannot
- You need publication-quality drawings: the drawing module is a thin matplotlib layer with force-directed layouts that look tangled beyond a few hundred nodes. Export to Graphviz, Gephi, or Cytoscape instead of fighting spring_layout
- You want parallelism: it is single-threaded Python and bound by the GIL. The nx-parallel and nx-cugraph backends exist and are genuinely useful, but they are separate installs covering a subset of algorithms, not a switch you flip for everything
- You need a persistent, queryable graph: this is an in-memory data structure with no indexing, no transactions, and no query language. A graph database is a different tool and NetworkX is not a substitute for one
- You are doing graph machine learning: PyTorch Geometric and DGL own that space, and while they read NetworkX graphs, building your pipeline around one is a detour
Setup reality
pip install networkx pulls nothing at all, which is the nice part and also the trap: converting to a SciPy sparse array, reading a DataFrame, or drawing anything raises ImportError until you install the optional stack. pip install networkx[default] adds numpy, scipy, matplotlib, and pandas and is what most people actually want. Graphviz layouts need pygraphviz, which compiles against system Graphviz headers and is the one dependency here that regularly fails to build; pydot is the easier fallback. Python support is 3.11 or newer, and the current release explicitly excludes 3.14.1. Two behaviours surprise newcomers: add_edge silently creates any node it has not seen, so a typo in a node name quietly adds a node instead of raising, and Graph collapses parallel edges, so keeping duplicates means choosing MultiGraph up front.
Patterns
Create a graph and attach attributesbuild-a-graph
import networkx as nx
G = nx.Graph()
G.add_node("alice", role="admin")
G.add_edge("alice", "bob", weight=4.0, since=2019)
G.add_edges_from([("bob", "carol", {"weight": 2.0})])
print(G.number_of_nodes(), G.number_of_edges())
print(G["alice"]["bob"]["weight"]) # 4.0
print(G.nodes["alice"]["role"]) # adminadd_edge creates missing nodes without complaining, so a misspelled name becomes a new node rather than an error. On a plain Graph, adding the same pair twice updates the existing edge instead of adding a second one.
Build a graph from a pandas DataFrameload-from-dataframe
import pandas as pd, networkx as nx
df = pd.read_csv("calls.csv") # columns: caller, callee, minutes
G = nx.from_pandas_edgelist(
df, source="caller", target="callee",
edge_attr=["minutes"], create_using=nx.DiGraph,
)
back = nx.to_pandas_edgelist(G)create_using must be the class, not an instance, or the direction is silently dropped. Needs pandas, which the bare install does not include.
Find shortest paths, weighted and unweightedshortest-paths
nx.shortest_path(G, "alice", "dave") # hop count, BFS
nx.shortest_path(G, "alice", "dave", weight="weight") # Dijkstra
nx.shortest_path_length(G, "alice", weight="weight") # dict from alice
for target, path in nx.single_source_dijkstra_path(G, "alice", cutoff=3).items():
print(target, path)Dijkstra rejects negative weights; use bellman_ford_path when costs can be negative. Missing endpoints raise NodeNotFound and disconnected ones raise NetworkXNoPath, so catch both rather than assuming a path exists.
Rank nodes by centralitycentrality
deg = nx.degree_centrality(G)
pr = nx.pagerank(G, alpha=0.85, weight="weight")
btw = nx.betweenness_centrality(G, k=500, seed=42, normalized=True)
top = sorted(pr, key=pr.get, reverse=True)[:10]Exact betweenness runs a shortest-path search from every node, so it becomes impractical well before other measures do. The k argument samples that many source nodes and needs a seed to be reproducible.
Split a graph into componentscomponents-and-connectivity
for comp in sorted(nx.connected_components(G), key=len, reverse=True):
print(len(comp))
largest = G.subgraph(max(nx.connected_components(G), key=len)).copy()
# directed graphs
list(nx.weakly_connected_components(D))
list(nx.strongly_connected_components(D))connected_components raises NetworkXNotImplemented on a directed graph; pick the weak or strong variant deliberately. subgraph returns a read-only view over the original, so call .copy() if you plan to mutate it.
Topologically sort a dependency graphdag-ordering
D = nx.DiGraph([("compile", "test"), ("test", "package"), ("lint", "package")])
if not nx.is_directed_acyclic_graph(D):
print(nx.find_cycle(D))
order = list(nx.topological_sort(D))
for batch in nx.topological_generations(D):
print("can run in parallel:", batch)topological_sort raises NetworkXUnfeasible on a cycle, and find_cycle raises NetworkXNoCycle when there is none, so both need handling. topological_generations is the one you want for scheduling parallel work.
Convert to a SciPy sparse array for linear algebrasparse-matrix-conversion
import numpy as np, networkx as nx
nodes = list(G)
A = nx.to_scipy_sparse_array(G, nodelist=nodes, weight="weight", format="csr")
deg = np.asarray(A.sum(axis=1)).ravel()
H = nx.from_scipy_sparse_array(A, create_using=nx.DiGraph)Always pass nodelist and keep it: row order follows it, and without it you cannot map matrix indices back to node names. from_scipy_sparse_array returns integer-labelled nodes, so relabel with nx.relabel_nodes if you need the original names.
Detect communitiescommunities
from networkx.algorithms.community import louvain_communities, modularity
comms = louvain_communities(G, weight="weight", resolution=1.0, seed=42)
print(len(comms), modularity(G, comms))Louvain is randomized: without seed you get different partitions on every run, which makes results look unstable when they are not. Raise resolution for more, smaller communities.
Save and load graphsserialize
import json, networkx as nx
nx.write_graphml(G, "graph.graphml")
G2 = nx.read_graphml("graph.graphml")
data = nx.node_link_data(G, edges="edges")
json.dump(data, open("graph.json", "w"))
G3 = nx.node_link_graph(data, edges="edges")The node_link key for edges is now 'edges'; older files and older code use 'links', so pass edges= explicitly on both sides when reading anything you saved before NetworkX 3.4. GraphML stringifies node ids, so integers come back as strings.
Draw a small graph with matplotlibdraw-a-graph
import matplotlib.pyplot as plt, networkx as nx
pos = nx.spring_layout(G, seed=42, k=0.3)
nx.draw_networkx_nodes(G, pos, node_size=300)
nx.draw_networkx_edges(G, pos, alpha=0.4)
nx.draw_networkx_labels(G, pos, font_size=8)
plt.axis("off")
plt.savefig("graph.png", dpi=200, bbox_inches="tight")spring_layout is random unless you pass a seed, so the same graph looks different every run. Past a few hundred nodes the output is an unreadable ball; export to Graphviz or Gephi instead.
Test two graphs for isomorphismcompare-graphs
from networkx.algorithms import isomorphism
nx.vf2pp_is_isomorphic(G1, G2, node_label="role")
matcher = isomorphism.GraphMatcher(
G1, G2, node_match=lambda a, b: a["role"] == b["role"])
if matcher.is_isomorphic():
print(matcher.mapping)Isomorphism is expensive in the general case and attribute matching narrows the search, so pass node_match or node_label when you can. vf2pp is the faster implementation for labelled graphs.
Dispatch an algorithm to a faster backenduse-a-backend
import networkx as nx
# per call
nx.betweenness_centrality(G, backend="parallel")
# or globally, with fallback to pure NetworkX
nx.config.backend_priority = ["cugraph"]
nx.config.fallback_to_nx = True
nx.config.cache_converted_graphs = TrueBackends such as nx-parallel, nx-cugraph, and graphblas-algorithms are separate installs and each covers only some algorithms. Graph conversion happens per call unless you turn on cache_converted_graphs, which can wipe out the speedup on small graphs.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| rustworkx | PyPI | You need the same kinds of algorithms on much larger graphs and can accept integer node indices; it is a Rust implementation with a deliberately similar API. |
| igraph | PyPI | You want a mature C core, fast community detection, and much better built-in plotting, and do not mind a less Pythonic interface. |
| networkit | PyPI | You are analysing very large networks and want parallel C++ algorithms for centrality and community detection across many cores. |
| graphblas-algorithms | PyPI | You want to keep writing NetworkX calls but run them over sparse linear algebra; it plugs in as a NetworkX backend. |