Topological Sort in Python With Kahn's Algorithm
Topological sort puts items in an order where every item comes after the items it depends on. Kahn's algorithm does it in O(V + E): count each node's unmet dependencies, repeatedly take a node with zero, and lower the count of the nodes that depend on it. If some nodes are never emitted, the graph has a cycle.
The real problem: apply order
A deploy tool that applies Kubernetes manifests in file order can apply a
Deployment before the ConfigMap it uses. The pod then fails with
configmap "web-config" not found. The fix is to sort the manifests so
dependencies come first:
web-config (ConfigMap) -> web-pod (Pod)
web-secret (Secret) -> web-pod (Pod)
This is a directed acyclic graph (DAG), and a dependency-first order of a DAG is a topological order. Build systems, package managers, task schedulers like Airflow, and spreadsheet recalculation use the same idea.
Kahn's algorithm, step by step
- For every node, count how many dependencies it is still waiting for. This is its in-degree.
- Put every node with in-degree
0into a "ready" queue. - Take a node from the queue and add it to the output.
- For every node that depends on it, lower its in-degree by 1. If it reaches
0, add it to the queue. - Repeat until the queue is empty.
- If the output has fewer nodes than the input, the remaining nodes are in a cycle - no valid order exists.
Python implementation
import heapq
from dataclasses import dataclass
@dataclass
class Manifest:
name: str
kind: str
depends_on: list[str]
def order_apply(manifests: list[Manifest]) -> list[Manifest]:
by_name = {m.name: m for m in manifests}
in_degree = {m.name: 0 for m in manifests}
dependents = {m.name: [] for m in manifests}
for m in manifests:
for dep in m.depends_on:
if dep in by_name: # ignore names not in the input
dependents[dep].append(m.name)
in_degree[m.name] += 1
# A min-heap gives a stable order: smallest name first when several are ready
ready = [name for name, d in in_degree.items() if d == 0]
heapq.heapify(ready)
out = []
while ready:
name = heapq.heappop(ready)
out.append(by_name[name])
for nxt in dependents[name]:
in_degree[nxt] -= 1
if in_degree[nxt] == 0:
heapq.heappush(ready, nxt)
if len(out) != len(manifests):
raise ValueError('cycle detected')
return out
Each node is pushed and popped once, and each edge is processed once, so the
work is O(V + E) plus the heap cost. A plain collections.deque is enough
if you do not need a fixed order between ready nodes.
Test the important cases
M = Manifest
# Diamond: d needs b and c, both need a
out = order_apply([M('d','Pod',['b','c']), M('b','X',['a']),
M('c','X',['a']), M('a','X',[])])
assert [m.name for m in out] == ['a', 'b', 'c', 'd']
# Cycle: a -> b -> c -> a
try:
order_apply([M('a','X',['c']), M('b','X',['a']), M('c','X',['b'])])
assert False, 'expected ValueError'
except ValueError:
pass
# Empty input
assert order_apply([]) == []
Also test a long chain (for example 1,000 nodes in shuffled input order). A correct O(V + E) implementation handles it instantly. A version that scans all nodes again on every step is O(V^2) and gets slow as the input grows.
Common mistakes
- Edges in the wrong direction. Store "who depends on me" (
dependents), not only "what I depend on", so step 4 is a direct lookup. - Forgetting cycle detection. Without the final length check, a cycle silently drops nodes from the output.
- Non-deterministic order. Iterating a set can change the order between runs. Use a heap or sort the ready list when stable output matters.
- Recursion limits. A DFS-based topological sort can hit Python's recursion limit on long chains. Kahn's algorithm is iterative and avoids it.
Standard library option
Since Python 3.9, graphlib.TopologicalSorter does this for you:
from graphlib import TopologicalSorter, CycleError
graph = {'web-pod': {'web-config', 'web-secret'}, 'web-config': set(), 'web-secret': set()}
print(list(TopologicalSorter(graph).static_order()))
It raises CycleError on a cycle. Knowing how to write Kahn's algorithm
yourself is still useful: it is a common interview question, and real tools
often need custom tie-breaking or partial ordering.
Do this project yourself. It's ready in a live cloud workspace in your browser - when you finish, it lands on your portfolio.
Start this project - freeWhat you'll practice
- Building an in-degree map and dependents list from a dependency list
- Implementing Kahn's algorithm with deterministic tie-breaking
- Detecting cycles and testing diamond, cycle, and empty inputs
FAQ
What is topological sort used for?
Ordering tasks that depend on each other: build steps, package installs, database migrations, Airflow tasks, and applying Kubernetes manifests so ConfigMaps and Secrets exist before the pods that use them.
What is the time complexity of Kahn's algorithm?
O(V + E), where V is the number of nodes and E the number of dependency edges. Each node and each edge is processed once.
How does Kahn's algorithm detect a cycle?
Nodes in a cycle never reach in-degree zero, so they are never emitted. If the output is shorter than the input, there is a cycle.
Is there a topological sort in the Python standard library?
Yes. graphlib.TopologicalSorter (Python 3.9+) returns a topological order with static_order() and raises CycleError on cycles.
Keep learning
Do this project yourself. It's ready in a live cloud workspace in your browser - when you finish, it lands on your portfolio.
Start this project - free