Programming Languages May 12, 2026 10 min read 22 views

Implementing BFS and DFS in Python: Hands-On Guide

Graph traversal is foundational to coding interviews and real-world systems. This hands-on guide teaches you implementing BFS and DFS in Python from scratch, with clear code, edge cases, and performance tips.

Hands-On Guide to Implementing BFS and DFS in Python

Graphs are everywhere — from social networks and navigation apps to compiler design and web crawling. If you want to solve problems involving connections, shortest paths, or searching through data that isn’t neatly linear, you need graph traversal. And in Python, two algorithms stand above the rest: BFS (Breadth-First Search) and DFS (Depth-First Search).

In this guide, you’ll learn implementing BFS and DFS in Python step by step. We’ll cover both recursive and iterative approaches, compare their time and space complexity, and show you exactly when to use each one. By the end, you’ll be ready to apply these algorithms to coding challenges, real-world projects, and technical interviews.

If you’re also interested in other foundational algorithms, check out our guide on Python Binary Search: Implementation Guide with Examples to strengthen your core algorithmic skills.


Why Graph Traversal Matters

Before writing code, let’s understand the problem. A graph consists of nodes (vertices) and edges connecting them. Traversal means visiting every reachable node in a systematic way.

  • BFS explores neighbors level by level (like ripples in a pond).
  • DFS goes as deep as possible down one path before backtracking (like exploring a maze).
    When you start implementing BFS and DFS in Python, you’re building a mental toolkit that applies to:
  • Web crawlers (DFS-style or BFS-style crawling)
  • GPS shortest paths (BFS for unweighted graphs)
  • Detecting cycles in dependency graphs
  • Solving puzzles (mazes, word ladders)
  • Social network friend recommendations
    We’ve written a separate deep dive on Real-World Applications of Graph Traversal Algorithms — make sure to read that after mastering the code here.

Representing Graphs in Python

To start implementing BFS and DFS in Python, you first need a way to represent a graph. The two most common representations are:

1. Adjacency List (Most common)

 

graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}

2. Adjacency Matrix (Good for dense graphs)

 

# 6x6 matrix for vertices A-F
matrix = [
    [0,1,1,0,0,0],
    [1,0,1,1,0,0],
    ...
]

For most coding interviews and practical scenarios, the adjacency list is the go-to choice. It’s memory-efficient and fast to iterate over neighbors.

Throughout this guide, we’ll use the adjacency list shown above.


BFS relies on a queue — first in, first out (FIFO). Python’s collections.deque is perfect for this.

Iterative BFS (Most Common)

 

Python

from collections import deque

def bfs(graph, start):
    visited = set()
    queue = deque([start])
    visited.add(start)

    order = []  # traversal order for demonstration

    while queue:
        vertex = queue.popleft()
        order.append(vertex)

        for neighbor in graph[vertex]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    return order

# Example usage
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}
print(bfs(graph, 'A'))  # Output: ['A', 'B', 'C', 'D', 'E', 'F']

 

BFS to Find Shortest Path (Unweighted Graph)

One major advantage of BFS is that it finds the shortest number of edges between nodes.

Python

def bfs_shortest_path(graph, start, goal):
    visited = set()
    queue = deque([(start, [start])])  # (node, path_so_far)

    while queue:
        vertex, path = queue.popleft()
        if vertex == goal:
            return path

        for neighbor in graph[vertex]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, path + [neighbor]))
    return None  # No path found

print(bfs_shortest_path(graph, 'A', 'F'))  # ['A', 'C', 'F'] or ['A', 'B', 'E', 'F']

When you’re implementing BFS and DFS in Python, remember: BFS guarantees the shortest path in unweighted graphs. DFS does not.


DFS can be implemented recursively (elegant but risks recursion depth limits) or iteratively (using a stack).

Recursive DFS (Simplest)

Python

def dfs_recursive(graph, node, visited=None, order=None):
    if visited is None:
        visited = set()
    if order is None:
        order = []

    visited.add(node)
    order.append(node)

    for neighbor in graph[node]:
        if neighbor not in visited:
            dfs_recursive(graph, neighbor, visited, order)
    return order

print(dfs_recursive(graph, 'A'))  # Output: ['A', 'B', 'D', 'E', 'F', 'C']

 

Iterative DFS (Safer for large graphs)

Python

def dfs_iterative(graph, start):
    visited = set()
    stack = [start]
    order = []

    while stack:
        vertex = stack.pop()
        if vertex not in visited:
            visited.add(vertex)
            order.append(vertex)
            # Add neighbors in reverse order to simulate recursive order
            for neighbor in reversed(graph[vertex]):
                if neighbor not in visited:
                    stack.append(neighbor)
    return order

print(dfs_iterative(graph, 'A'))  # Output: ['A', 'B', 'D', 'E', 'F', 'C']

 

The iterative version avoids recursion depth limits and is often preferred in production code. Both approaches are valid when implementing BFS and DFS in Python — choose based on your constraints.


BFS vs DFS: Key Differences and When to Use Which

Architectural Comparison: BFS vs. DFS

FeatureBreadth-First Search (BFS)Depth-First Search (DFS)
Underlying Data StructureQueue (First-In, First-Out / FIFO)Stack (Last-In, First-Out / LIFO) or System Call Stack (via recursion)
Space Complexity

$O(V)$ worst-case


 

(proportional to the maximum width of the graph)

$O(V)$ worst-case for skewed graphs;


 

$O(\log V)$ for balanced trees (proportional to maximum depth)

Shortest Path GuaranteeYes (guaranteed for unweighted graphs)No (finds a valid path, not necessarily the shortest)
Completeness

Complete


 

(always finds a solution if one exists)

Conditional


 

(may hang in infinite paths/cycles if visited states are not tracked)

Primary Use CasesNearest-neighbor tracking, level-order traversals, peer-to-peer networks, routing algorithms.Topological sorting, cycle detection, maze routing, constraint satisfaction puzzles.

Key Architectural Trade-offs

  • Memory Footprint: BFS holds an entire layer of the graph in memory at once, making it memory-intensive for high-branching factor structures. DFS only holds the path from the root to the current node, maintaining a significantly lighter footprint in deep, narrow structures.

  • Search Optimality: BFS evaluates nodes objectively by distance from the origin, meaning it discovers the optimal path first. DFS commits blindly to a single path, meaning it may waste computation exploring a dead end millions of nodes deep when the target was adjacent to the starting node.

When to choose BFS:

  • Finding shortest path in unweighted graphs (e.g., social distance)
  • Web crawling level-by-level
  • GPS navigation with equal edge weights
    When to choose DFS:
  • Detecting cycles in a graph
  • Topological sorting (prerequisite graphs)
  • Solving mazes or puzzles where you want any path
  • Memory-constrained environments (iterative DFS can be more memory efficient than BFS in sparse graphs)
    Understanding these tradeoffs is critical when implementing BFS and DFS in Python for real problems. For a deeper look at performance, see our guide on Mastering Time Complexity in Python: A Complete Guide.

Advanced BFS and DFS Patterns

1. BFS on a Grid (Matrix)

Graphs aren’t always explicit. A 2D grid is a graph where each cell connects to its neighbors.

 

Python

from collections import deque

def bfs_grid(grid, start, target):
    rows, cols = len(grid), len(grid[0])
    directions = [(0,1), (1,0), (0,-1), (-1,0)]
    visited = set()
    queue = deque([(start[0], start[1], 0)])  # (r, c, distance)

    while queue:
        r, c, dist = queue.popleft()
        if (r, c) == target:
            return dist

        for dr, dc in directions:
            nr, nc = r + dr, c + dc
            if 0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in visited:
                visited.add((nr, nc))
                queue.append((nr, nc, dist + 1))
    return -1

grid = [[0,0,0],[0,1,0],[0,0,0]]
print(bfs_grid(grid, (0,0), (2,2)))  # Manhattan distance ignoring obstacles

 

2. DFS for Cycle Detection in Directed Graph

 

Python

def has_cycle_directed(graph):
    visited = set()
    recursion_stack = set()

    def dfs(node):
        visited.add(node)
        recursion_stack.add(node)

        for neighbor in graph[node]:
            if neighbor not in visited:
                if dfs(neighbor):
                    return True
            elif neighbor in recursion_stack:
                return True

        recursion_stack.remove(node)
        return False

    for node in graph:
        if node not in visited:
            if dfs(node):
                return True
    return False

 

3. DFS for Connected Components

Python

def connected_components(graph):
    visited = set()
    components = []

    def dfs(start, component):
        stack = [start]
        while stack:
            node = stack.pop()
            if node not in visited:
                visited.add(node)
                component.append(node)
                for neighbor in graph[node]:
                    if neighbor not in visited:
                        stack.append(neighbor)

    for node in graph:
        if node not in visited:
            comp = []
            dfs(node, comp)
            components.append(comp)
    return components

print(connected_components(graph))

 

These patterns appear constantly in coding challenges. If you’re preparing for interviews, also check out Top Graph Algorithm Interview Questions & Answers.


Common Mistakes When Implementing BFS and DFS in Python

Even experienced developers make errors. Here’s what to watch for:

1. Forgetting to Mark Visited Before Enqueue (BFS)

Python

# WRONG
if neighbor not in visited:
    queue.append(neighbor)   # ← visited added too late
    visited.add(neighbor)    # can cause duplicates in queue

# RIGHT
if neighbor not in visited:
    visited.add(neighbor)
    queue.append(neighbor)

 

2. Using List as a Queue (BFS)

Python

# WRONG (O(n) pop(0))
queue = []
queue.append(x)
item = queue.pop(0)  # SLOW!

# RIGHT
from collections import deque
queue = deque([start])
item = queue.popleft()

 

3. Recursion Depth Limit (DFS)

Python’s default recursion limit is ~1000. For large graphs, use iterative DFS or increase limit:

Python

import sys
sys.setrecursionlimit(1000000)

 

4. Modifying Graph While Iterating

Python

# WRONG
for neighbor in graph[node]:
    graph[node].append(new_node)  # changes size during iteration

# RIGHT - create a copy or use a separate structure

 

These pitfalls are similar to those in other algorithms. Read Common Mistakes in Binary Search Implementation to see how traversal errors compare.


Performance Analysis for BFS and DFS in Python

Time Complexity

Both BFS and DFS visit each vertex and edge once:

  • O(V + E) for adjacency list
  • O(V²) for adjacency matrix

Space Complexity

  • BFS: O(V) for queue + visited set. In worst case (dense graph), queue holds most vertices.
  • DFS iterative: O(V) for stack + visited
  • DFS recursive: O(V) call stack depth (could overflow)
    When implementing BFS and DFS in Python, always consider the graph density. BFS can use significantly more memory if the graph is wide (many neighbors per node).

For a deeper understanding of analyzing algorithm efficiency, explore Mastering Time Complexity Analysis for Data Structures and How to Calculate Big O Notation for Beginners.


Real-World Practice Problems

Try these challenges to solidify your skills:

  1. Word Ladder (LeetCode 127) – BFS on a graph where words are nodes and edges connect words differing by one letter.
  2. Number of Islands (LeetCode 200) – DFS or BFS on a grid to count connected components.
  3. Course Schedule (LeetCode 207) – DFS cycle detection in a directed graph.
  4. Pacific Atlantic Water Flow (LeetCode 417) – Multi-source BFS.
  5. Clone Graph (LeetCode 133) – BFS/DFS with a hashmap.
    Implementing BFS and DFS in Python for these problems will prepare you for technical screens. Pair your practice with our Master Algorithm Implementation for Coding Interviews | Guide.

Debugging Graph Traversal Code

Even correct algorithms can fail due to subtle bugs. Here are debugging strategies:


Next Steps After Implementing BFS and DFS in Python

You’ve learned the core traversal algorithms. Now extend your knowledge:


Frequently Asked Questions

1. Can I use BFS and DFS on directed graphs exactly the same way?

Yes, both algorithms work unchanged on directed graphs. The only difference is that you only traverse edges in their given direction. The adjacency list representation remains the same.

2. Which is faster, BFS or DFS?

Neither is universally faster. Both are O(V+E). BFS can be slower in practice due to queue overhead, but it finds shortest paths. DFS may be faster if the solution is deep and the graph is narrow. Choose based on the problem, not raw speed.

3. How do I handle very large graphs when implementing BFS and DFS in Python?

Use iterative versions to avoid recursion limits. Consider using sys.setrecursionlimit() if recursion is necessary. For massive graphs that don’t fit in memory, use disk-based traversal or graph databases like Neo4j.

4. What’s the difference between DFS and backtracking?

DFS is a complete graph traversal algorithm. Backtracking is a problem-solving technique that uses DFS implicitly but prunes branches that can’t lead to a solution. Backtracking often modifies state as it goes (e.g., N-Queens).

5. Do I need to implement both BFS and DFS from scratch in interviews?

Yes, many interviews require whiteboarding one of them. Master both iterative and recursive versions. Also know when to use one over the other. Practice explaining the tradeoffs clearly.


Conclusion

You’ve just completed a comprehensive guide to implementing BFS and DFS in Python. You now understand:

  • How to represent graphs using adjacency lists
  • BFS with deque for shortest paths and level-order traversal
  • DFS both recursively and iteratively using a stack
  • Key differences, complexity analysis, and common pitfalls
  • Real-world patterns like grid BFS and cycle detection
     

Graph traversal is a gateway to solving complex relationship problems. Whether you’re preparing for FAANG interviews, building a recommendation engine, or exploring social network analysis, BFS and DFS will be your reliable tools.

Keep practicing with coding challenges, and explore our related resources:

Now open your editor, create a graph, and start traversing. The connections are waiting.



Related Posts

Binary Search Explained: Algorithm, Examples, & Edge Cases

Master the binary search algorithm with clear, step-by-step examples. Learn how to implement efficient searches in sorted arrays, avoid common …

Mar 11, 2026
How to Approach Hard LeetCode Problems | A Strategic Framework

Master the mental framework and strategies to confidently break down and solve even the most challenging LeetCode problems.

Mar 06, 2026
Two Pointer Technique | Master Array Problems in 8 Steps

Master the two-pointer technique to solve complex array and string problems efficiently. This guide breaks down patterns, provides step-by-step examples, …

Mar 11, 2026

Need Coding Help?

Get expert assistance with your programming assignments and projects.