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

    Python code for safe context in asynchronous applications
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python contextvars for Safe Context

    Learn how Python contextvars isolates request data across asyncio tasks, logs, threads, and tests without unsafe global variables.

    Ler mais

    Tempo de leitura: 7 minutos
    26/07/2026
    Python code using cached_property to store expensive calculations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python cached_property: Cache Object Values

    Learn Python cached_property to store expensive calculations, invalidate values, and prevent stale object caches.

    Ler mais

    Tempo de leitura: 6 minutos
    25/07/2026
    Python code with type-based function dispatch
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python singledispatch: Type-Based Functions

    Learn Python singledispatch to build type-based functions, reduce isinstance chains, and organize extensible polymorphism with clear examples.

    Ler mais

    Tempo de leitura: 6 minutos
    25/07/2026
    Python code demonstrating descriptors and attributes
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Descriptors: Practical Guide

    Learn Python descriptors with __get__, __set__, validation, property, instance storage, testing, inheritance and practical design tips.

    Ler mais

    Tempo de leitura: 6 minutos
    22/07/2026
    Caixas empilhadas
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Modules and Packages: Complete Guide

    Learn how to create Python modules and packages, organize imports, use __init__.py, run modules, avoid circular imports, and structure real

    Ler mais

    Tempo de leitura: 5 minutos
    10/07/2026
    logo do python com objetos abaixo do logo
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Object-Oriented Python: Classes and Objects

    Learn object-oriented Python with classes, objects, attributes, methods, constructors, inheritance, composition, encapsulation, properties, and practical examples.

    Ler mais

    Tempo de leitura: 6 minutos
    10/07/2026