DiGraph vs MultiDiGraph: When Parallel Edges Matter in NetworkX
Use DiGraph for single directed relationships and MultiDiGraph for parallel edges, but expect keyed lookups and higher memory costs.
18 Oct 2026, 22:12 UTC

The problem: the same directed edge behaves differently depending on the graph class
\nYou are modeling a directed system\u2014perhaps API dependencies, message routing, or task prerequisites\u2014and you add the same relationship twice with different attributes. On one graph class the second call silently replaces the first. On another it creates a second, parallel edge. If your downstream code assumes one edge per node pair, the wrong choice corrupts metrics or raises exceptions at lookup time.
\nNetworkX offers two directed graph classes for this decision: DiGraph, which stores at most one edge for each ordered pair of nodes, and MultiDiGraph, which stores parallel edges between the same endpoints. This behavior is stable across recent NetworkX releases, including 3.x.
DiGraph overwrites; MultiDiGraph accumulates
\nA parallel edge is a second edge with the same source and target but its own identity and attributes. In a DiGraph, calling add_edge(u, v) twice overwrites the first edge's attributes. In a MultiDiGraph, each call creates a new edge distinguished by an integer edge key, and each key carries its own attribute dictionary.
This distinction matters for self-loops too: DiGraph holds one self-loop per node, while MultiDiGraph can hold multiple self-loops on the same node, each with a different key.
Access patterns change when keys appear
\nOn a DiGraph, edge attributes are at G[u][v]. On a MultiDiGraph, G[u][v] returns a mapping of keys to attribute dictionaries, so you need G[u][v][key] to reach one edge's data. Neighbor iteration also differs: MultiDiGraph yields (neighbor, key) pairs, while DiGraph yields neighbors only.
Worked example: duplicate routing hops
\nRun this in a python3 REPL or save it as graph_choice.py. No special permissions are required because the graph exists only in memory.
import networkx as nx\n\nG = nx.MultiDiGraph()\nG.add_edge(\"gateway\", \"auth\", attempt=1, latency_ms=35)\nG.add_edge(\"gateway\", \"auth\", attempt=2, latency_ms=110)\n\n# Verification: confirm two parallel edges are retained.\nprint(G.edges(keys=True, data=True))\n\n# Verification: confirm the keyed access structure.\nprint(type(G[\"gateway\"][\"auth\"]))\n\nCheck that the edge list contains two entries for (\"gateway\", \"auth\") with distinct keys, and that type(G[\"gateway\"][\"auth\"]) is a dict-like mapping keyed by edge identifier rather than a flat attribute dict. A risk is reusing code written for DiGraph: G[\"gateway\"][\"auth\"][\"latency_ms\"] will raise KeyError on a MultiDiGraph because the intermediate value is the key mapping, not the attribute dict.
To start over, reassign G = nx.MultiDiGraph(); the graph is in-memory and discarding it removes all edges.
Trade-offs and limitations
\n- \n
- Memory and traversal:
MultiDiGraphstores every parallel edge independently, so it uses more memory and costs more to traverse thanDiGraphfor the same node count. \n - Code compatibility: generic graph algorithms that assume simple neighbor iteration or
G[u][v]attribute access may break or return unexpected structures onMultiDiGraph. \n - Conversion collapse: converting a
MultiDiGraphtoDiGraphcan silently drop parallel edges and attributes. Inspect edge counts before and after conversion. \n
Practical verification
\n- \n
- Build a
MultiDiGraph, add the same edge twice with different attributes, and inspectG.edges(keys=True, data=True)to confirm both edges remain. \n - Compare
type(G[u][v])and iteration overG[u]on aDiGraphand aMultiDiGraphto confirm the keyed structure difference. \n - Run
nx.info(G)before and after adding parallel edges to observe how edge counts diverge between the two classes. \n
Decision checklist
\n- \n
- Choose
DiGraphwhen each ordered pair represents one relationship and you want lighter weight, simpler lookups. \n - Choose
MultiDiGraphwhen the same source and target can have multiple meaningful relationships, such as retries, redundant channels, or versioned dependencies. \n - If you choose
MultiDiGraph, audit every place your code reads edges or iterates neighbors to account for the extra key dimension. \n
Start with DiGraph unless you have a concrete need for parallel edges. The extra flexibility of MultiDiGraph is useful, but it shifts the burden of edge identity onto every consumer of your graph.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.