The Algorithms - Python Python TheAlgorithms/Python

How Bellman-Ford Works

Relax every edge V-1 times, then run one more pass to detect negative cycles. The algorithm Dijkstra cannot replace.

6 stops ~16 min Verified 2026-05-05
What you will learn
  • Why Dijkstra fails on negative edge weights and Bellman-Ford handles them
  • How initializing all distances to infinity and the source to 0.0 establishes the relaxation invariant
  • Why exactly V-1 outer iterations guarantee shortest paths in any graph without a negative cycle
  • How the inner edge scan extracts src, dst, weight from each dict and applies the relaxation test
  • How check_negative_cycle runs a V-th relaxation pass to detect any cycle whose total weight is negative
  • How the doctest constructs the edge list inline and verifies both the normal output and the negative-cycle exception
Prerequisites
  • Familiarity with Python list comprehensions and dict access
  • A basic mental model of a weighted directed graph
  • No prior knowledge of shortest-path algorithms required
The story behind Bellman-Ford

Richard Bellman published a shortest-path algorithm in 1958 and Lester Ford Jr. described an equivalent formulation in 1956, independently, at RAND Corporation. The algorithm appeared under combined credit from the 1960s onward. The key capability that distinguishes it from Dijkstra is handling negative edge weights. Dijkstra's priority-queue invariant assumes that once a vertex is popped at cost c, no later path reaches it for less, but that assumption breaks when a negative edge can reduce a path cost after the vertex was finalized. Bellman-Ford avoids that assumption by relaxing every edge on every iteration rather than processing vertices in cost order.

The price is runtime: O(VE) versus Dijkstra's O((V + E) log V). The payoff is generality. Bellman-Ford is the basis of distance-vector routing protocols, including RIP (Routing Information Protocol) and portions of EIGRP, where routers exchange distance tables with neighbors and update local tables iteratively, exactly as Bellman-Ford relaxes edges in repeated passes. The negative-cycle detection feature matters for financial applications where arbitrage opportunities appear as negative-weight cycles in a currency-exchange graph. No shortest path through a negative cycle is well-defined, so detecting and rejecting the graph is the correct behavior.

1 / 6

print_distance: A Helper Separate from the Algorithm

graphs/bellman_ford.py:1

The file opens with a future-annotations import and a print helper. Separating output from computation follows the contribution guide's no-print-inside-algorithm rule.

The contribution guide requires algorithm functions to return results rather than print them. print_distance on line 4 is the separation point: the bellman_ford function returns a list of distances and prints nothing, while this helper consumes that list for human-readable display. The from __future__ import annotations on line 1 enables PEP 563 deferred annotation evaluation, which is used elsewhere in this repository for type hints that reference types not yet defined at parse time. The helper itself is three lines: a header row, then a tab-separated vertex-index and distance for every entry in the distance list. Having it as a standalone function rather than embedded in __main__ keeps the block at the bottom of the file short and makes it callable from other modules without dragging in print side effects.

Key takeaway

Output is separated from computation. The algorithm returns a list; a helper prints it. That split is what the contribution guide's no-print rule asks for.

from __future__ import annotations


def print_distance(distance: list[float], src):
    print(f"Vertex\tShortest Distance from vertex {src}")
    for i, d in enumerate(distance):
        print(f"{i}\t\t{d}")
2 / 6

check_negative_cycle: The V-th Relaxation Pass

graphs/bellman_ford.py:10

A V-th edge scan detects any edge where relaxation still improves a distance, which proves a negative cycle exists.

After V-1 relaxation passes, the distance array holds shortest paths for any graph without a negative cycle. The proof is the edge-count invariant: after k passes, every shortest path using at most k edges is correct, and no acyclic shortest path needs more than V-1 edges. If any edge still improves a distance after V-1 passes, the only explanation is a negative cycle reachable from the source. check_negative_cycle runs exactly one more pass over every edge on line 13. The unpacking on line 14 pulls "src", "dst", and "weight" from each dict in one expression. The distance[u] != float("inf") guard on line 15 skips unreachable vertices, because infinity plus any finite weight is still infinity and no cycle through an unreachable vertex matters.

Key takeaway

A V-th edge pass after convergence is the negative-cycle test. Any edge that still relaxes proves a cycle, because V-1 passes are enough for all acyclic paths.

def check_negative_cycle(
    graph: list[dict[str, int]], distance: list[float], edge_count: int
):
    for j in range(edge_count):
        u, v, w = (graph[j][k] for k in ["src", "dst", "weight"])
        if distance[u] != float("inf") and distance[u] + w < distance[v]:
            return True
    return False
3 / 6

bellman_ford: Signature and Doctest Contract

graphs/bellman_ford.py:20

The function takes a flat edge-dict list, vertex count, edge count, and source vertex. Two doctests pin normal output and the negative-cycle exception.

The signature takes four arguments because the graph format here is not an adjacency dict but a flat list of edge dicts. Each dict has keys "src", "dst", and "weight". The explicit vertex_count and edge_count parameters are required because the flat edge list does not expose the vertex count, and the outer loop iterates exactly vertex_count - 1 times. The first doctest constructs a four-vertex graph with one negative edge (weight -10 from vertex 2 to vertex 1) and expects distances [0.0, -2.0, 8.0, 5.0] from source 0. The second doctest adds one more edge that creates a negative cycle and expects the Exception: Negative cycle found traceback. The Traceback (most recent call last): ... pattern on lines 32 through 34 is the standard doctest format for testing that an exception is raised.

Key takeaway

The flat edge-dict format requires explicit vertex and edge counts. The second doctest verifies negative-cycle detection via a standard exception-matching pattern.

def bellman_ford(
    graph: list[dict[str, int]], vertex_count: int, edge_count: int, src: int
) -> list[float]:
    """
    Returns shortest paths from a vertex src to all
    other vertices.
    >>> edges = [(2, 1, -10), (3, 2, 3), (0, 3, 5), (0, 1, 4)]
    >>> g = [{"src": s, "dst": d, "weight": w} for s, d, w in edges]
    >>> bellman_ford(g, 4, 4, 0)
    [0.0, -2.0, 8.0, 5.0]
    >>> g = [{"src": s, "dst": d, "weight": w} for s, d, w in edges + [(1, 3, 5)]]
    >>> bellman_ford(g, 4, 5, 0)
    Traceback (most recent call last):
     ...
    Exception: Negative cycle found
    """
4 / 6

Distance Initialization and the V-1 Relaxation Loop

graphs/bellman_ford.py:36

All distances start at infinity; source is 0.0. V-1 outer iterations relax every edge, finding all shortest paths with no negative cycles.

Line 36 initializes every vertex's distance to positive infinity, signaling that no path is yet known. Line 37 sets the source to 0.0, matching the list[float] return type. The outer loop on line 39 runs exactly vertex_count - 1 times: the maximum edge count on any shortest acyclic path. The inner loop on line 40 walks every edge. Line 41 extracts src, dst, and weight from the current edge dict using the same generator pattern as check_negative_cycle. Line 43 is the relaxation test: if vertex u is reachable and the path through it is cheaper than the current distance to v, line 44 updates distance[v]. The absence of a priority queue or visited set is what produces O(VE) rather than Dijkstra's O((V + E) log V).

Key takeaway

V-1 outer iterations times E inner edge relaxations gives O(VE). No priority queue, no visited set. Every reachable shortest path is found regardless of edge-weight signs.

    distance = [float("inf")] * vertex_count
    distance[src] = 0.0

    for _ in range(vertex_count - 1):
        for j in range(edge_count):
            u, v, w = (graph[j][k] for k in ["src", "dst", "weight"])

            if distance[u] != float("inf") and distance[u] + w < distance[v]:
                distance[v] = distance[u] + w
5 / 6

Negative-Cycle Check and Return

graphs/bellman_ford.py:46

After V-1 passes, check_negative_cycle runs the V-th pass. An exception signals the cycle; otherwise the distance list is returned.

Lines 46 through 50 are the completion contract: run the V-th pass via check_negative_cycle, raise if a cycle is found, return the distance list if not. Raising an exception rather than returning a sentinel value is the contribution guide's preferred pattern for conditions that invalidate the result. A caller who catches this exception knows the graph is malformed for the purpose of single-source shortest paths. A caller who does not catch it gets a clear stack trace rather than a silently wrong distance table. The return type list[float] uses float rather than int because the initialization uses float("inf") and Python's type system propagates that to the entries that remain infinite when a vertex is unreachable from the source.

Key takeaway

Raising Exception("Negative cycle found") is the right choice. Returning a sentinel would leave the caller with a silently wrong distance table.

    negative_cycle_exists = check_negative_cycle(graph, distance, edge_count)
    if negative_cycle_exists:
        raise Exception("Negative cycle found")

    return distance
6 / 6

The __main__ Block: doctest.testmod() and an Interactive Graph Builder

graphs/bellman_ford.py:53

The __main__ block runs the in-docstring examples first, then interactively builds a graph edge by edge from stdin before running the algorithm.

The __main__ block follows the same pattern as graphs/dijkstra.py: run doctest.testmod() first so any regression in the docstring examples surfaces immediately on direct invocation, then drop into an interactive session. The interactive session builds the graph edge by edge from stdin. Lines 65 through 67 read space-separated source, destination, and weight for each edge and pack them into the dict format the algorithm expects. The loop on line 63 prompts the user for each edge with a human-readable label. Line 69 asks for the source vertex after all edges are entered, then calls bellman_ford and passes the result to print_distance. Notice the print_distance call on line 73 passes 0 as the source label rather than the actual source variable, which is a minor inconsistency in the file: the displayed header will always read "Shortest Distance from vertex 0" regardless of what source was entered.

Key takeaway

doctest.testmod() before the interactive session catches docstring drift locally. The interactive graph builder mirrors the edge-dict format the algorithm consumes.

if __name__ == "__main__":
    import doctest

    doctest.testmod()

    V = int(input("Enter number of vertices: ").strip())
    E = int(input("Enter number of edges: ").strip())

    graph: list[dict[str, int]] = [{} for _ in range(E)]

    for i in range(E):
        print("Edge ", i + 1)
        src, dest, weight = (
            int(x)
            for x in input("Enter source, destination, weight: ").strip().split(" ")
        )
        graph[i] = {"src": src, "dst": dest, "weight": weight}

    source = int(input("\nEnter shortest path source:").strip())
    shortest_distance = bellman_ford(graph, V, E, source)
    print_distance(shortest_distance, 0)
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