The Algorithms - Python Python TheAlgorithms/Python

How DFS Works

Follow a path as deep as it goes before backtracking. The iterative stack-based implementation that mirrors the recursive structure without blowing the call stack.

5 stops ~12 min Verified 2026-05-05
What you will learn
  • Why DFS follows one path to its end before exploring siblings, the opposite expansion order from BFS
  • How a plain Python list used as a stack produces LIFO (last in, first out) expansion with pop()
  • How explored is initialized with the start vertex to handle self-loops before the loop begins
  • Why reversed(graph[v]) preserves the intuitive left-to-right exploration order when using a stack
  • How the doctest verifies all-vertex reachability without pinning traversal order by using a membership check
  • How DFS and BFS share the same visited-set contract but differ only in frontier data structure
Prerequisites
  • Comfort reading Python while loops and list operations
  • A mental model of stacks (last in, first out)
  • No prior knowledge of depth-first search required
The story behind depth-first search

Depth-first search as a graph algorithm was implicit in backtracking methods used by maze solvers for centuries, but its formal analysis came with Robert Tarjan's 1972 paper "Depth-First Search and Linear Graph Algorithms," published in the SIAM Journal on Computing. Tarjan showed that a single DFS pass over a directed graph classifies every edge into one of four types: tree edges, back edges, forward edges, and cross edges. That classification is the foundation of his linear-time algorithm for strongly connected components, which is itself the basis of topological sort, cycle detection, and compiler analysis of recursive function calls.

The recursive formulation of DFS is the one most textbooks present because it matches the call stack directly: each recursive call goes one level deeper, and returning from the call is the backtrack. The iterative version presented here replaces the call stack with an explicit Python list used as a stack, which avoids Python's default recursion limit of 1,000 frames for large graphs. Both versions share the same invariant: a vertex is explored when it is first encountered, and its neighbors are added to the frontier so the deepest one is visited next.

1 / 5

Module Docstring and Imports

graphs/depth_first_search.py:1

A one-line module docstring declares the implementation strategy (non-recursive), and from __future__ import annotations enables deferred type hints for the function signature.

The module docstring is one sentence: "Non recursive implementation of a DFS algorithm." That sentence is load-bearing. Most descriptions of DFS start from the recursive version because the call stack naturally models the backtracking. This file takes the opposite approach: it uses an explicit Python list as a stack, which means it can traverse graphs with tens of thousands of nodes without hitting Python's default recursion depth limit of 1,000 frames. Choosing the iterative form is a pedagogical decision about which correctness constraint to make explicit. In the recursive form, the call stack is the frontier and backtracking is implicit in function return. Here, the stack is a named variable and backtracking is stack.pop(). The from __future__ import annotations import on line 3 defers evaluation of the type hints in the function signature, which allows using dict and set as generic type aliases under Python 3.14's syntax rules.

Key takeaway

"Non recursive" in the docstring is a design commitment. The explicit stack avoids the 1,000-frame recursion limit and makes the frontier visible as a named variable.

"""Non recursive implementation of a DFS algorithm."""

from __future__ import annotations
2 / 5

depth_first_search: Signature, Docstring, and the Two Doctests

graphs/depth_first_search.py:6

The function takes a plain dict graph and a start vertex string. Two doctests verify full reachability from different starts without pinning traversal order.

The graph format here is simpler than the Graph class in breadth_first_search.py: a plain dict whose keys are vertex names (strings here, not integers) and whose values are neighbor lists. Passing the graph as a dict rather than constructing a class instance means the caller controls the graph layout entirely. The two doctests sidestep traversal-order nondeterminism by checking membership rather than equality: all(x in output_G for x in ...) asserts that every vertex returned by DFS is in the expected set of all seven vertices. Running from vertex A and from vertex G checks that any starting point in a connected graph reaches every other vertex. The graph contains a self-loop (D lists D as a neighbor) and a back edge (B to A), both of which the visited set must handle.

Key takeaway

Testing with all(x in expected for x in result) verifies complete reachability without requiring a specific traversal order, which is nondeterministic for DFS.

def depth_first_search(graph: dict, start: str) -> set[str]:
    """Depth First Search on Graph
    :param graph: directed graph in dictionary format
    :param start: starting vertex as a string
    :returns: the trace of the search
    >>> input_G = { "A": ["B", "C", "D"], "B": ["A", "D", "E"],
    ... "C": ["A", "F"], "D": ["B", "D"], "E": ["B", "F"],
    ... "F": ["C", "E", "G"], "G": ["F"] }
    >>> output_G = list({'A', 'B', 'C', 'D', 'E', 'F', 'G'})
    >>> all(x in output_G for x in list(depth_first_search(input_G, "A")))
    True
    >>> all(x in output_G for x in list(depth_first_search(input_G, "G")))
    True
    """
3 / 5

The Iterative Core: Stack, explored Set, and reversed()

graphs/depth_first_search.py:20

explored and stack both start with the start vertex. The loop pops LIFO, marks the vertex explored, and pushes neighbors in reversed order.

Line 20 is the most compact initialization in the file: set(start) produces a one-element set only because every vertex here is a single character (set('AB') would split into {'A','B'}), and [start] is a one-element list serving as the stack. Initializing explored with the start vertex before the loop handles the self-loop case: if the start vertex has itself as a neighbor, the check on line 29 blocks it from being pushed again. The stack.pop() on line 23 removes the last element (LIFO), which is the defining difference from BFS. The reversed(graph[v]) on line 28 is a deliberate ordering choice: pushing neighbors in reversed order means the first neighbor in the adjacency list ends up on top of the stack and is therefore explored first, preserving the left-to-right expansion order. Without reversing, the rightmost neighbor would be visited first.

Key takeaway

stack.pop() gives LIFO expansion. reversed(graph[v]) pushes neighbors so the first adjacency-list neighbor reaches the top and is explored first.

    explored, stack = set(start), [start]

    while stack:
        v = stack.pop()
        explored.add(v)
        # Differences from BFS:
        # 1) pop last element instead of first one
        # 2) add adjacent elements to stack without exploring them
        for adj in reversed(graph[v]):
            if adj not in explored:
                stack.append(adj)
    return explored
4 / 5

The Module-Level Graph G and the __main__ Block

graphs/depth_first_search.py:34

Module-level G defines the seven-vertex test graph. The __main__ block validates doctests then prints a live DFS result from vertex A.

The module-level G dict on lines 34 through 42 is the same graph used in the doctest. Defining it at module level rather than inside __main__ means it is available to any importer and matches the dict the doctest constructs inline, so a reader can compare them side by side. The graph is undirected in spirit but represented as a directed adjacency list: vertex A lists B, C, D as neighbors, and vertex B lists A back. Vertex D lists D, the self-loop the doctest relies on. The __main__ block runs doctest.testmod() first, which validates the two membership assertions in the docstring, then prints the actual traversal result of depth_first_search(G, "A"). Because sets are unordered, the printed output will vary across Python versions and runs; the doctest avoids this by not asserting the set contents directly.

Key takeaway

Module-level G matches the doctest graph, so a reader comparing them sees the same structure. The __main__ print shows the actual traversal set without asserting its order.

G = {
    "A": ["B", "C", "D"],
    "B": ["A", "D", "E"],
    "C": ["A", "F"],
    "D": ["B", "D"],
    "E": ["B", "F"],
    "F": ["C", "E", "G"],
    "G": ["F"],
}

if __name__ == "__main__":
    import doctest

    doctest.testmod()
    print(depth_first_search(G, "A"))
5 / 5

BFS vs DFS: The One-Line Structural Difference

graphs/depth_first_search.py:20

BFS and DFS share the same visited-set pattern. The only structural difference is queue.get() versus stack.pop(), switching expansion from FIFO to LIFO.

The code comment on lines 25 through 27 is explicit about what distinguishes this loop from BFS. The two numbered differences the comment names are: (1) pop from the last position instead of the front, and (2) add adjacent elements to the frontier without marking them as explored at that moment. The BFS implementation in breadth_first_search.py uses queue.put() and queue.get() with a Queue object; this file uses stack.append() and stack.pop() on a plain list. Both check the visited structure before adding to the frontier. Both return a set of all reachable vertices. The difference is which vertex each picks next: BFS picks the oldest frontier member (shortest distance first), DFS picks the newest (deepest path first). That single choice determines whether the traversal guarantees shortest paths or exhaustive depth-first coverage.

Key takeaway

The entire BFS-to-DFS conversion is swapping queue.get() for stack.pop(). Everything else, including the visited-set check, stays identical.

    explored, stack = set(start), [start]

    while stack:
        v = stack.pop()
        explored.add(v)
        # Differences from BFS:
        # 1) pop last element instead of first one
        # 2) add adjacent elements to stack without exploring them
        for adj in reversed(graph[v]):
            if adj not in explored:
                stack.append(adj)
    return explored
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