The Algorithms - Python Python TheAlgorithms/Python

How Dijkstra's Algorithm Works

A min-heap of (cost, node) tuples, a visited set, and a single relaxation loop. The 1956 algorithm at the core of OSPF and IS-IS routing.

7 stops ~18 min Verified 2026-05-05
What you will learn
  • Why a min-heap of (cost, node) tuples gives the next vertex to visit in O(log n)
  • How the visited set turns a potentially exponential search into one pass per vertex
  • How the inner relaxation loop pushes (cost + edge_weight, neighbor) onto the heap
  • Why the algorithm returns the destination cost as soon as the destination is popped
  • Why Dijkstra fails on graphs with negative edge weights and Bellman-Ford handles them
  • How the example graphs G, G2, and G3 prove the doctest expectations
  • Where this implementation differs from a production routing daemon's incremental updates
Prerequisites
  • Familiarity with Python's heapq module or any binary heap
  • Comfort reading nested loops over an adjacency dict
  • No prior knowledge of shortest-path algorithms required
The story behind Dijkstra's algorithm

Edsger Dijkstra invented the algorithm in 1956 in about twenty minutes at a sidewalk café in Amsterdam, while he and his fiancée were waiting for coffee and he was wondering how to take a tram from Rotterdam to Groningen with the fewest changes. He worked it out without paper. He published it three years later, in 1959, as a half-page note in Numerische Mathematik titled "A note on two problems in connexion with graphs." The original formulation used a flat list of distances; the priority-queue version that every textbook now teaches came later, from work by Donald Johnson and others in the 1970s.

The reason the algorithm is everywhere is that the relaxation invariant generalizes. Every link-state routing protocol that runs the modern internet (OSPF, IS-IS, OLSR for mesh networks) computes shortest paths with Dijkstra over a graph the routers exchange among themselves. The catch is that the algorithm assumes non-negative edge weights. Routes with negative cost (or arbitrage cycles in finance) need Bellman-Ford instead, which trades runtime for the ability to detect a negative cycle. For the non-negative case, Dijkstra remains the fastest practical choice.

1 / 7

Module Docstring: The Pseudocode and the Invariant

graphs/dijkstra.py:1

The module docstring spells out the algorithm in pseudocode and explains why the min-heap invariant guarantees correctness on non-negative edge weights.

A reader who has never seen Dijkstra before needs the algorithm in plain language, not Python, and the module docstring delivers it. Lines 4 through 23 give a thirteen-step pseudocode listing that names every variable the implementation uses below. Lines 24 through 31 then explain the load-bearing invariant: a min-heap returns the next vertex with the shortest path-cost-from-start, so once a vertex is popped, no later path can reach it more cheaply. That invariant is what makes the visited-set check correct. The implementation that follows is twenty-six lines of Python and reads as a transliteration of this pseudocode, which is the canonical pattern for graph algorithms in this repository: pseudocode first, code second, doctest third.

Key takeaway

The min-heap invariant is the load-bearing claim: once a vertex pops with cost c, no later path reaches it for less. That is what makes the visited set correct.

"""
pseudo-code

DIJKSTRA(graph G, start vertex s, destination vertex d):

//all nodes initially unexplored

1 -  let H = min heap data structure, initialized with 0 and s [here 0 indicates
     the distance from start vertex s]
2 -  while H is non-empty:
3 -    remove the first node and cost of H, call it U and cost
4 -    if U has been previously explored:
5 -      go to the while loop, line 2 //Once a node is explored there is no need
         to make it again
6 -    mark U as explored
7 -    if U is d:
8 -      return cost // total cost from start to destination vertex
9 -    for each edge(U, V): c=cost of edge(U,V) // for V in graph[U]
10 -     if V explored:
11 -       go to next V in line 9
12 -     total_cost = cost + c
13 -     add (total_cost,V) to H

You can think at cost as a distance where Dijkstra finds the shortest distance
between vertices s and v in a graph G. The use of a min heap as H guarantees
that if a vertex has already been explored there will be no other path with
shortest distance, that happens because heapq.heappop will always return the
next vertex with the shortest distance, considering that the heap stores not
only the distance between previous vertex and current vertex but the entire
distance between each vertex that makes up the path from start vertex to target
vertex.
"""
2 / 7

Imports, Signature, and the Doctest Contract

graphs/dijkstra.py:34

The function imports heapq, takes a graph dict and two vertex names, and the doctest pins three test graphs against expected shortest-path costs.

Dijkstra needs a priority queue keyed by path cost, and Python's standard library ships one as heapq. That single import on line 34 is the only dependency the algorithm has; the heap is used as a min-priority queue with tuples that compare lexicographically (cost first, vertex second). The signature on line 37 takes three arguments: an adjacency-dict graph, a start vertex name, and an end vertex name. Notice that the function returns the shortest-path cost as a number rather than the path itself, which is enough for routing and queries but means a caller who needs the path must run a second pass with parent pointers. Three doctests pin the contract against expected costs of 6, 3, and 3, so a regression on the heap or visited logic fails CI before the PR can merge.

Key takeaway

The function returns the cost, not the path. heapq is the only dependency; three doctests pin the contract against three different graphs.

import heapq


def dijkstra(graph, start, end):
    """Return the cost of the shortest path between vertices start and end.

    >>> dijkstra(G, "E", "C")
    6
    >>> dijkstra(G2, "E", "F")
    3
    >>> dijkstra(G3, "E", "F")
    3
    """
3 / 7

Heap Init, Visited Set, and the Outer Loop

graphs/dijkstra.py:48

The heap starts with (0, start). The outer loop pops the cheapest unvisited vertex, marks it visited, and returns immediately if it is the destination.

Three structures hold the entire state. Line 48 seeds the min-heap with a single tuple (0, start); the cost is zero because we have not yet moved. Line 49 is the visited set, empty at first. The outer loop on line 50 then repeatedly pops the cheapest unvisited vertex. The visited check on line 52 is what handles a wrinkle in this implementation: the same vertex may be pushed multiple times (once per incoming edge that improved the cost), so duplicates land in the heap and the visited set is what skips them on the second pop. Line 54 commits the current vertex as visited at its final cost, and line 55 returns immediately if that vertex is the destination. The algorithm exits the moment end is popped, which can be much sooner than visiting the entire graph.

Key takeaway

The heap may hold the same vertex multiple times; the visited set dedupes. Returning on the first pop of end short-circuits the search.

    heap = [(0, start)]  # cost from start node,end node
    visited = set()
    while heap:
        (cost, u) = heapq.heappop(heap)
        if u in visited:
            continue
        visited.add(u)
        if u == end:
            return cost
4 / 7

The Relaxation Step: Push Every Improving Neighbor

graphs/dijkstra.py:57

For each unvisited neighbor of the current vertex, push (current_cost + edge_weight, neighbor) onto the heap. Return -1 if the heap drains.

Relaxation is the step that actually walks the graph. For every neighbor of the current vertex, the algorithm computes cost + c (the cost to reach the current vertex plus the edge weight to the neighbor) and pushes that tuple onto the heap. The visited check on line 58 skips neighbors that already have their final cost, which would otherwise cause useless pushes. There is no per-vertex distance-tracking dictionary in this implementation, no decrease-key operation; the cheapest path to any vertex is whichever copy of that vertex pops first. The trade-off is a heap that can hold up to E entries instead of V, but heappush and heappop both stay at O(log E) and the algorithm still runs in O((V + E) log V) overall. Line 62 returns -1 if the heap drains without reaching end, which is how unreachable destinations are signaled.

Key takeaway

Push every improving neighbor onto the heap; the visited check skips already-finalized vertices. Return -1 means no path exists from start to end.

        for v, c in graph[u]:
            if v in visited:
                continue
            next_item = cost + c
            heapq.heappush(heap, (next_item, v))
    return -1
5 / 7

Graph G: An Adjacency Dict in Plain Python

graphs/dijkstra.py:65

Graph G maps each vertex to a list of [neighbor, edge-weight] pairs. The doctest expects dijkstra(G, E, C) to return 6.

The graph format the algorithm consumes is the simplest one Python supports: a dict whose keys are vertex names and whose values are lists of [neighbor, edge_weight] pairs. Reading vertex E on line 70 shows it has two outgoing edges, one to B with weight 4 and one to F with weight 3. The doctest at the top of the file expects dijkstra(G, "E", "C") to return 6. Tracing by hand: from E the cheapest path is E to F (cost 3), then F to C (cost 3), totaling 6. The direct path E to B to A to C costs 4 + 2 + 5 = 11. Dijkstra finds the cheaper path because the heap pops F before B even though B is a direct neighbor, since 3 is less than 4.

Key takeaway

Adjacency dicts are the native graph format. Tracing the heap pops by hand recovers the doctest: 3 + 3 = 6 beats 4 + 2 + 5.

G = {
    "A": [["B", 2], ["C", 5]],
    "B": [["A", 2], ["D", 3], ["E", 1], ["F", 1]],
    "C": [["A", 5], ["F", 3]],
    "D": [["B", 3]],
    "E": [["B", 4], ["F", 3]],
    "F": [["C", 3], ["E", 3]],
}
6 / 7

Graph G2: ASCII Layout and the Tie-Break

graphs/dijkstra.py:74

An ASCII diagram above G2 shows a 5-vertex line and a weight-3 shortcut. The doctest expects cost 3.

The ASCII diagram on lines 75 through 81 is what makes this file read like a textbook chapter rather than a code listing. It draws G2 as a line of five vertices with weight-1 edges, plus a single weight-3 shortcut edge from E to F. Below the diagram, the dict on lines 82 through 88 encodes the same graph in the format the algorithm reads. A doctest then expects dijkstra(G2, "E", "F") to return 3 because the shortcut and the long path through B, C, D both have total cost 3, and Dijkstra correctly returns 3 either way. Together, the diagram and the dict make the test self-explanatory; a contributor who edits the algorithm and breaks the tie-break logic immediately sees which graph caught the regression by reading the picture above the failing test.

Key takeaway

An ASCII diagram next to the dict is the documentation pattern this file uses. The reader sees the topology and the test expectation in one screen.

r"""
Layout of G2:

E -- 1 --> B -- 1 --> C -- 1 --> D -- 1 --> F
 \                                         /\
  \                                        ||
    ----------------- 3 --------------------
"""
G2 = {
    "B": [["C", 1]],
    "C": [["D", 1]],
    "D": [["F", 1]],
    "E": [["B", 1], ["F", 3]],
    "F": [],
}
7 / 7

The __main__ Guard Runs the Doctests

graphs/dijkstra.py:116

Direct execution runs doctest.testmod(), which validates all three graph examples in the function docstring before the script exits.

Like merge_sort.py and unlike quick_sort.py, this file runs doctest.testmod() the moment it is invoked directly. That means a contributor running python3 graphs/dijkstra.py sees every doctest in the function docstring execute against the in-file graphs G, G2, and G3, with any mismatch raising a clear failure message. The file already calls dijkstra three times on lines 107, 110, and 113 with print statements, so execution prints 6, 3, and 3 above the doctest summary line. This is the canonical shape for a graph-algorithm file in this repository: import, function, in-file example graphs, calls that print expected output, and a doctest runner under __main__. The Bellman-Ford file at graphs/bellman_ford.py follows the same five-section pattern.

Key takeaway

Running the file directly executes doctest.testmod() against the in-file graphs. The five-section pattern (import, function, examples, prints, __main__ doctest) recurs across graphs/.

if __name__ == "__main__":
    import doctest

    doctest.testmod()
Your codebase next

Create code tours for your project

Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.

Install Intraview Free