Python Graphs and Graph Traversal

Represent graphs with adjacency lists and explore them with breadth-first and depth-first search, handling cycles and disconnected vertices.

Lesson 88 of 107Advanced13 min read

A graph is a set of vertices (also called nodes) connected by edges. Unlike a tree, a graph can contain cycles and a vertex can be reached by several routes. Road maps, social networks, web links and trail networks are all graphs. In Python, the usual representation is an adjacency list: a dictionary mapping each vertex to its neighbours.

Python
trails = {
    "Gate": ["Pond", "Hill"],
    "Pond": ["Gate", "Woods"],
    "Hill": ["Gate", "Woods"],
    "Woods": ["Pond", "Hill"],
}

This lesson covers representing graphs, why traversals need a visited set, breadth-first search (BFS) for the fewest-hops route, and depth-first search (DFS) for exploring deeply and finding connected parts. The examples navigate the trails of a nature reserve. It builds on stacks and queues and trees.

Vertices and edges

Decide what vertices and edges mean before writing code: here, vertices are trail junctions and an edge means a walkable trail between two junctions.

  • Undirected edges work both ways, so each one appears in both vertices' lists, as in trails above.
  • Directed edges go one way, like a one-way boardwalk: "Pond": ["Woods"] without "Woods": ["Pond"].
  • Weighted edges carry a cost, such as distance: {"Gate": {"Pond": 1.2, "Hill": 0.8}}.
Python
from collections import defaultdict
edges = [("Gate", "Pond"), ("Gate", "Hill"), ("Pond", "Woods"), ("Hill", "Woods")]
graph = defaultdict(list)
for a, b in edges:
    graph[a].append(b)
    graph[b].append(a)          # omit this line for a directed graph

An adjacency list uses memory proportional to vertices plus edges and lets you loop over a vertex's neighbours quickly, which is what traversals need. An adjacency matrix, an n × n grid of 0s and 1s, answers "is there an edge between A and B?" in O(1) but uses n² memory, so it suits small or dense graphs.

Visited state

In a graph with cycles, a traversal that simply follows edges would loop forever: Gate → Pond → Gate → Pond... Every traversal therefore keeps a visited set and skips vertices it has already seen:

Python
seen = {"Gate"}
for neighbor in trails["Gate"]:
    if neighbor not in seen:
        seen.add(neighbor)
        # schedule neighbor for exploration

A set gives O(1) average membership checks, so the whole traversal runs in O(V + E) time: each vertex is processed once and each edge examined a constant number of times.

When you mark a vertex matters. In BFS, mark it as seen when you add it to the queue, not when you take it out. Marking late lets the same vertex be queued several times by different neighbours, wasting work and, in BFS, potentially recording the wrong distance. Disconnected vertices are never reached from the start vertex at all; to visit every part of a graph, start a new traversal from each vertex not yet seen.

Independent practice and a worked check

Task: implement BFS and DFS for a trail graph with a cycle and a disconnected vertex.

Python
from collections import deque

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

def bfs(start):
    order, seen, queue = [], {start}, deque([start])
    while queue:
        v = queue.popleft()
        order.append(v)
        for n in graph[v]:
            if n not in seen:
                seen.add(n)
                queue.append(n)
    return order

def dfs(start, seen=None):
    seen = seen if seen is not None else []
    seen.append(start)
    for n in graph[start]:
        if n not in seen:
            dfs(n, seen)
    return seen

print(bfs("A"))   # ['A', 'B', 'C', 'D']
print(dfs("A"))   # ['A', 'B', 'D', 'C']
print(bfs("E"))   # ['E']: the disconnected vertex is never reached from A

The cycle A–B–D–C–A doesn't cause an infinite loop because both traversals skip seen vertices. For the worked check, the traversal that finds shortest hop counts in an unweighted graph is breadth-first search.

Reference and next practice

The NetworkX documentation covers graph algorithms in depth, and Python's graphlib module provides topological sorting. Continue with recursive problem solving. Practise with the Python exercises and Python quiz.

Graphs and Graph Traversal Summary

  • Vertices and edges: A graph models entities and their connections, which may be directed or undirected.
  • Visited state: A visited set prevents revisiting vertices indefinitely in graphs with cycles.
  • Breadth first search: BFS explores by distance in edge count using a queue.
  • Depth first search: DFS explores a branch before alternatives and can use a stack or recursion.

Graphs and Graph Traversal Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Graphs and Graph Traversal in a small program

Python
from collections import deque
graph = {"Gate": ["Pond", "Hill"], "Pond": ["Gate"], "Hill": ["Gate"]}
queue = deque(["Gate"])
seen = {"Gate"}
while queue:
    place = queue.popleft()
    print(place)
    for neighbor in graph[place]:
        if neighbor not in seen:
            seen.add(neighbor)
            queue.append(neighbor)
Output
Gate
Pond
Hill
Line-by-line explanation
  1. Line 2The adjacency list: each place maps to the places directly connected to it.
  2. Lines 3 to 4BFS starts with Gate in the queue and already marked as seen.
  3. Lines 5 to 11Each place taken from the front of the queue adds its unseen neighbours to the back.
Example 2

Shortest hops, a route and connected parts

Python
from collections import deque

trails = {
    "Gate": ["Pond", "Hill"],
    "Pond": ["Gate", "Woods"],
    "Hill": ["Gate", "Woods", "Ridge"],
    "Woods": ["Pond", "Hill"],
    "Ridge": ["Hill"],
    "Island": [],                       # reachable only by boat
}

def bfs_route(graph, start, goal):
    previous = {start: None}
    queue = deque([start])
    while queue:
        vertex = queue.popleft()
        if vertex == goal:
            break
        for neighbor in graph[vertex]:
            if neighbor not in previous:
                previous[neighbor] = vertex
                queue.append(neighbor)
    if goal not in previous:
        return None
    route, step = [], goal
    while step is not None:
        route.append(step)
        step = previous[step]
    return route[::-1]

def components(graph):
    seen, parts = set(), []
    for start in graph:
        if start in seen:
            continue
        stack, part = [start], []
        seen.add(start)
        while stack:
            vertex = stack.pop()
            part.append(vertex)
            for neighbor in graph[vertex]:
                if neighbor not in seen:
                    seen.add(neighbor)
                    stack.append(neighbor)
        parts.append(sorted(part))
    return parts

print(bfs_route(trails, "Gate", "Ridge"))
print(bfs_route(trails, "Gate", "Island"))
print(components(trails))
Output
['Gate', 'Hill', 'Ridge']
None
[['Gate', 'Hill', 'Pond', 'Ridge', 'Woods'], ['Island']]
Line-by-line explanation
  1. Lines 13 to 22BFS records each vertex's predecessor the first time it's reached.
  2. Lines 23 to 29Following predecessors back from the goal rebuilds the route; an unreached goal returns None.
  3. Lines 31 to 45A DFS from every unseen vertex finds each connected part, including the isolated Island.

Common Graphs and Graph Traversal Mistakes

Traversing a graph without a visited set

Don't
def explore(graph, vertex):
    for neighbor in graph[vertex]:
        explore(graph, neighbor)       # Gate -> Pond -> Gate -> ... RecursionError
Do
def explore(graph, vertex, seen=None):
    seen = seen if seen is not None else set()
    seen.add(vertex)
    for neighbor in graph[vertex]:
        if neighbor not in seen:
            explore(graph, neighbor, seen)
    return seen

Using BFS for weighted distances

Don't
# trails with lengths in km; BFS counts edges, not kilometres
route = bfs_route(weighted_trails, "Gate", "Woods")
Do
import heapq   # Dijkstra's algorithm: always expand the closest unvisited vertex

BFS gives the fewest edges. When edges have lengths, use Dijkstra's algorithm with a priority queue.

Graphs and Graph Traversal in Real Projects

Graphs in real applications

Navigation apps run weighted shortest-path algorithms on road graphs. Social networks use BFS to suggest "friends of friends", web crawlers traverse links, package managers and build tools topologically sort dependency graphs (Python's graphlib.TopologicalSorter does this), and fraud detection looks for unusual connected groups of accounts.

For serious graph work in Python, the NetworkX library implements BFS, DFS, Dijkstra, connected components, centrality measures and much more on top of the same adjacency ideas. Writing the core traversals yourself, as in this lesson, makes those library functions much easier to understand and use correctly.

Graphs and Graph Traversal FAQ

What is the difference between BFS and DFS?

BFS explores level by level using a queue and finds the fewest-edges path in an unweighted graph. DFS goes deep along one branch using a stack or recursion, and suits cycle detection, connected components and topological sorting. Both visit every reachable vertex in O(V + E).

How do I represent a graph in Python?

Most often as an adjacency list: a dictionary mapping each vertex to a list or set of neighbours. Use a dictionary of dictionaries for weighted edges, and an adjacency matrix only for small or dense graphs.

Why does my graph traversal loop forever?

The graph has a cycle and the traversal revisits vertices. Keep a seen set and skip any neighbour already in it; in BFS, add vertices to seen when you enqueue them.

How do I find the shortest path in a weighted graph?

Use Dijkstra's algorithm, which repeatedly expands the closest unvisited vertex using a priority queue (heapq). It requires non-negative weights. NetworkX's shortest_path(G, a, b, weight="weight") implements it.

What is a connected component?

A group of vertices that can all reach each other, with no edges to vertices outside the group. Running a traversal from every not-yet-seen vertex finds all components, including isolated vertices.

Graphs and Graph Traversal Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Which traversal finds shortest hop counts in an unweighted graph?

Multiple choicemedium

Which review concern is valid for graphs and graph traversal?

Graphs and Graph Traversal Quiz

2 questions to check what you learned.

  1. Which traversal finds shortest hop counts in an unweighted graph?

    • Any depth first traversal
    • Breadth first search
    • Python ignores the distinction
  2. Which statement correctly describes breadth first search?

    • BFS explores by distance in edge count using a queue.
    • Only the example output matters, not the rule
    • Every operation has the same behavior