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.
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
trailsabove. - 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}}.
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 graphAn 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:
seen = {"Gate"}
for neighbor in trails["Gate"]:
if neighbor not in seen:
seen.add(neighbor)
# schedule neighbor for explorationA 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.
Breadth first search
BFS explores outward in rings: first the start vertex, then everything one edge away, then everything two edges away, and so on. It uses a queue:
from collections import deque
def hops_from(graph, start):
distance = {start: 0}
queue = deque([start])
while queue:
vertex = queue.popleft()
for neighbor in graph[vertex]:
if neighbor not in distance:
distance[neighbor] = distance[vertex] + 1
queue.append(neighbor)
return distanceBecause BFS reaches vertices in order of distance, the first time it finds a vertex is via a route with the fewest edges. That makes it the algorithm for shortest paths in unweighted graphs, such as "fewest trail junctions between the gate and the woods". Recording each vertex's predecessor lets you reconstruct the route.
BFS doesn't find the shortest distance when edges have different lengths; a route with fewer but longer trails may be counted as shorter. Weighted shortest paths need Dijkstra's algorithm, which replaces the queue with a priority queue (heapq).
Depth first search
DFS follows one branch as far as it can before backing up and trying alternatives. It uses a stack, either explicitly or through recursion:
def dfs(graph, start):
order, seen, stack = [], set(), [start]
while stack:
vertex = stack.pop()
if vertex in seen:
continue
seen.add(vertex)
order.append(vertex)
for neighbor in reversed(graph[vertex]): # visit in listed order
if neighbor not in seen:
stack.append(neighbor)
return orderDFS is the basis for finding connected components, detecting cycles, topological sorting of tasks with dependencies, and solving mazes and puzzles. It doesn't find shortest paths.
The visiting order depends on the order neighbours are listed; change the adjacency list order and DFS (and BFS) visit vertices in a different sequence, though they reach the same set of vertices. A recursive DFS is shorter to write but limited by Python's recursion depth on long paths, so the explicit stack version is safer for large graphs.
Independent practice and a worked check
Task: implement BFS and DFS for a trail graph with a cycle and a disconnected vertex.
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 AThe 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.
Graphs and Graph Traversal in a small program
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)Gate
Pond
HillLine-by-line explanation
- Line 2The adjacency list: each place maps to the places directly connected to it.
- Lines 3 to 4BFS starts with Gate in the queue and already marked as seen.
- Lines 5 to 11Each place taken from the front of the queue adds its unseen neighbours to the back.
Shortest hops, a route and connected parts
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))['Gate', 'Hill', 'Ridge']
None
[['Gate', 'Hill', 'Pond', 'Ridge', 'Woods'], ['Island']]Line-by-line explanation
- Lines 13 to 22BFS records each vertex's predecessor the first time it's reached.
- Lines 23 to 29Following predecessors back from the goal rebuilds the route; an unreached goal returns
None. - 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
def explore(graph, vertex):
for neighbor in graph[vertex]:
explore(graph, neighbor) # Gate -> Pond -> Gate -> ... RecursionErrordef 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 seenUsing BFS for weighted distances
# trails with lengths in km; BFS counts edges, not kilometres
route = bfs_route(weighted_trails, "Gate", "Woods")import heapq # Dijkstra's algorithm: always expand the closest unvisited vertexBFS 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.
Which traversal finds shortest hop counts in an unweighted graph?
Which review concern is valid for graphs and graph traversal?
Graphs and Graph Traversal Quiz
2 questions to check what you learned.
Which traversal finds shortest hop counts in an unweighted graph?
- Any depth first traversal
- Breadth first search
- Python ignores the distinction
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