Python graphlib: Topological Sorting

Published on: July 27, 2026
Reading time: 5 minutes
Software dependency graph and Python task workflow

Many software workflows contain tasks that cannot start until other tasks have finished. A package depends on libraries, a deployment depends on tests, a database migration depends on the previous migration, and a data pipeline must extract information before transforming it. These relationships form a directed dependency graph. The standard-library Python graphlib module provides TopologicalSorter, a focused tool for producing a valid execution order, finding work that can run concurrently, and detecting circular dependencies.

This guide explains how to model predecessors, generate a static order, coordinate parallel workers, diagnose cycles, validate task names, and test a dependency graph. The topic complements practical guides about Python contextvars, priority queues with heapq, cached_property, and singledispatch.

What is topological sorting?

A topological order is a linear sequence of vertices in a directed graph where every prerequisite appears before the item that depends on it. If deploy depends on test, the test step must occur earlier in the sequence. A graph can have several valid topological orders when independent tasks are available at the same time.

A valid order exists only for a directed acyclic graph. A cycle appears when task A depends on B, B depends on C, and C eventually depends on A. No task in that loop can start, because each one waits for another member of the same cycle. TopologicalSorter reports this problem with CycleError.

Representing dependencies

The constructor accepts a dictionary that maps each node to an iterable of its predecessors. This direction is important: the values are requirements, not successors.

from graphlib import TopologicalSorter

dependencies = {
    "deploy": {"test"},
    "test": {"build"},
    "build": {"install"},
    "install": set(),
}

order = list(TopologicalSorter(dependencies).static_order())
print(order)

The resulting sequence respects every relationship. The exact output may differ when two nodes do not depend on each other, and applications should not rely on an arbitrary tie order.

Using static_order

For a sequential workflow, static_order() is the simplest interface. It prepares the graph, checks for cycles, and returns an iterator over the nodes:

from graphlib import TopologicalSorter

steps = {
    "send_email": {"generate_report"},
    "generate_report": {"query_database", "load_config"},
    "query_database": {"load_config"},
    "load_config": set(),
}

for step in TopologicalSorter(steps).static_order():
    print(f"Running: {step}")

This approach is ideal for validation tools, small scripts, migration plans, and workflows that intentionally execute one task at a time.

Adding nodes incrementally

You can also create an empty sorter and register dependencies with add(). The first argument is the node and the remaining arguments are its predecessors:

from graphlib import TopologicalSorter

sorter = TopologicalSorter()
sorter.add("build", "lint", "test")
sorter.add("deploy", "build")
sorter.add("lint")
sorter.add("test", "install")
sorter.add("install")

print(list(sorter.static_order()))

Repeated calls for the same node accumulate prerequisites. This is useful when plugins or modules contribute parts of a larger pipeline. Once preparation begins, however, the graph should no longer be modified.

Incremental and parallel processing

The most powerful use of TopologicalSorter is coordinating tasks that can run concurrently. The relevant methods are prepare(), get_ready(), done(), and is_active().

from graphlib import TopologicalSorter

steps = {
    "deploy": {"tests", "frontend_build"},
    "tests": {"install"},
    "frontend_build": {"install"},
    "install": set(),
}

sorter = TopologicalSorter(steps)
sorter.prepare()

while sorter.is_active():
    ready = sorter.get_ready()
    for task in ready:
        print("Running", task)
        # run the task and wait for success
        sorter.done(task)

After install is marked complete, both tests and frontend_build become ready. They may be submitted to threads, processes, containers, or remote workers. Calling done() releases downstream nodes only after the application confirms completion.

Using ThreadPoolExecutor

For input/output-heavy steps, a thread pool can process independent nodes concurrently:

from concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED
from graphlib import TopologicalSorter
import time

steps = {
    "publish": {"test", "document"},
    "test": {"compile"},
    "document": {"compile"},
    "compile": {"download"},
    "download": set(),
}

def run(name):
    print("start", name)
    time.sleep(0.2)
    print("finish", name)
    return name

sorter = TopologicalSorter(steps)
sorter.prepare()
futures = {}

with ThreadPoolExecutor(max_workers=3) as executor:
    while sorter.is_active():
        for task in sorter.get_ready():
            future = executor.submit(run, task)
            futures[future] = task

        completed, _ = wait(futures, return_when=FIRST_COMPLETED)
        for future in completed:
            task = futures.pop(future)
            future.result()
            sorter.done(task)

A production implementation must decide what happens after a failure. Do not call done() when a prerequisite failed unless the workflow explicitly allows downstream execution. Common policies include stopping the graph, retrying the task, marking dependents as skipped, or recording a partial result.

Detecting circular dependencies

Both prepare() and iteration through static_order() can raise CycleError. The exception arguments include information about one detected cycle:

from graphlib import CycleError, TopologicalSorter

graph = {
    "a": {"c"},
    "b": {"a"},
    "c": {"b"},
}

try:
    print(list(TopologicalSorter(graph).static_order()))
except CycleError as error:
    print("Circular dependency:", error.args)

A sorter may still expose nodes outside the cycle before progress becomes impossible. Diagnostic tools can use that behavior to report both the valid portion and the blocked component.

Implicit predecessor nodes

If a predecessor is referenced but does not appear as a dictionary key, graphlib automatically adds it as a node with no prerequisites. This is convenient, but it can hide spelling mistakes. For example, configuration and configuraton become separate nodes.

Validate all referenced identifiers against an approved catalog:

catalog = {"config", "extract", "transform", "load"}

dependencies = {
    "transform": {"extract"},
    "load": {"transform", "config"},
}

referenced = set(dependencies)
for predecessors in dependencies.values():
    referenced.update(predecessors)

unknown = referenced - catalog
if unknown:
    raise ValueError(f"Unknown tasks: {unknown}")

Using objects as nodes

Nodes must be hashable, but they do not have to be strings. Enums, tuples, integers, and immutable data classes can carry richer metadata:

from dataclasses import dataclass
from graphlib import TopologicalSorter

@dataclass(frozen=True)
class Task:
    name: str
    team: str

extract = Task("extract", "data")
validate = Task("validate", "quality")
publish = Task("publish", "platform")

graph = {
    validate: {extract},
    publish: {validate},
}

for task in TopologicalSorter(graph).static_order():
    print(task.name, task.team)

Avoid mutable objects whose equality or hash may change after insertion. Changing hashed state can make dictionary and set behavior unpredictable.

Practical applications

Topological sorting is useful for:

  • package and plugin dependency resolution;
  • data engineering and machine-learning pipelines;
  • database migration ordering;
  • module compilation;
  • course prerequisite planning;
  • spreadsheet and report generation;
  • service startup sequencing;
  • build systems and deployment workflows.

The module determines readiness and order. It does not provide persistence, distributed queues, resource quotas, automatic retries, schedules, or monitoring. A larger system can use it as the planning core while another component executes and records tasks.

Testing a dependency graph

When multiple valid orders exist, testing one exact list makes the test unnecessarily fragile. Instead, verify the defining property: every predecessor must appear before the dependent node.

from graphlib import TopologicalSorter

def assert_valid_order(graph):
    order = list(TopologicalSorter(graph).static_order())
    position = {node: index for index, node in enumerate(order)}

    for node, predecessors in graph.items():
        for predecessor in predecessors:
            assert position[predecessor] < position[node]

assert_valid_order({
    "deploy": {"test", "build"},
    "test": {"install"},
    "build": {"install"},
    "install": set(),
})

Add tests for an empty graph, an isolated node, several independent roots, direct and indirect cycles, duplicate registrations, unknown identifiers, and worker failures.

Common mistakes

  • Providing successors instead of predecessors.
  • Assuming independent nodes always have the same relative order.
  • Forgetting to call done() during incremental processing.
  • Calling done() before work actually succeeds.
  • Trying to modify the graph after preparation.
  • Ignoring worker exceptions and releasing dependents.
  • Using inconsistent names that create unintended nodes.
  • Expecting graphlib to execute tasks automatically.
  • Use stable, immutable node identifiers.
  • Validate unknown nodes before preparation.
  • Document that the mapping points from nodes to predecessors.
  • Separate planning, execution, and persistence.
  • Define failure and retry policies before adding concurrency.
  • Limit parallelism according to CPU, memory, and external services.
  • Record task start, finish, duration, and outcome.
  • Test graph properties rather than arbitrary tie ordering.

Conclusion

Python graphlib turns dependency relationships into a valid execution plan without third-party packages. static_order() handles straightforward sequential cases, while prepare(), get_ready(), done(), and is_active() support concurrent scheduling.

The essential design work is modeling predecessors accurately and treating cycles as configuration errors. TopologicalSorter is not a complete workflow orchestrator, but it is a reliable foundation for installers, build systems, migrations, data pipelines, and any process governed by prerequisites.

For version-specific behavior, consult the official graphlib documentation and the concurrent.futures documentation.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Folders and directories for Python contextlib.chdir
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    contextlib.chdir: Restore Directories Automatically

    Learn Python contextlib.chdir to change directories temporarily, restore paths safely, isolate tests, and avoid global-state bugs.

    Ler mais

    Tempo de leitura: 5 minutos
    03/09/2026
    Python code execution and performance monitoring
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    sys.monitoring: Low-Overhead Instrumentation

    Learn Python sys.monitoring for low-overhead instrumentation with selective events, callbacks, tooling, and safe observability.

    Ler mais

    Tempo de leitura: 5 minutos
    03/09/2026
    Software developer organizing object data with Python operator.attrgetter
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    operator.attrgetter: Sort Objects by Attributes

    Learn Python operator.attrgetter to sort, group, and transform objects by simple or nested attributes with clearer reusable code.

    Ler mais

    Tempo de leitura: 4 minutos
    02/09/2026
    Asynchronous programming with Python asyncio.Runner
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    asyncio.Runner: Reuse the Event Loop Safely

    Learn Python asyncio.Runner to reuse an event loop, control context, signals, debug mode, cancellation, and safe asynchronous shutdown.

    Ler mais

    Tempo de leitura: 6 minutos
    02/09/2026
    Binary data compression with Zstandard in Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: Zstandard Streams and Dictionaries

    Learn Python compression.zstd for Zstandard compression, streaming, dictionaries, safe limits, testing, and production workflows.

    Ler mais

    Tempo de leitura: 6 minutos
    01/09/2026
    Python application packaged as an executable zipapp archive
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python zipapp: Build Executable Apps

    Learn Python zipapp to package applications as executable pyz archives, include dependencies, and distribute tools safely.

    Ler mais

    Tempo de leitura: 5 minutos
    01/09/2026