TopologicalSorter: Order Dependencies Safely

Published on: September 6, 2026
Reading time: 6 minutes
Software dependency graph and task workflow with Python TopologicalSorter

graphlib.TopologicalSorter is a Python standard-library class for ordering dependencies in a directed acyclic graph. It solves a common production problem: a task may run only after all of its prerequisites have completed. Build systems, data pipelines, database migrations, report generation, deployment workflows, and file-processing applications all need this kind of ordering.

Instead of maintaining a fragile handwritten sequence, you describe which nodes depend on which predecessors. TopologicalSorter calculates a valid order and also provides an incremental API that can feed ready tasks to parallel workers without violating dependencies.

What topological ordering means

A topological order is a sequence in which every dependency appears before the item that depends on it. Imagine four steps: download data, validate it, transform it, and publish it. Validation depends on download, transformation depends on validation, and publication depends on transformation. The valid sequence is obvious in this small example, but larger graphs often contain several independent tasks that may be executed at the same time.

Topological sorting works only for graphs without cycles. If task A depends on B while B depends on A, no valid order exists. Python detects this condition and raises CycleError.

Your first static_order example

from graphlib import TopologicalSorter

graph = {
    "publish": {"transform"},
    "transform": {"validate"},
    "validate": {"download"},
    "download": set(),
}

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

The dictionary maps each task to its set of predecessors. static_order() prepares the graph, checks for cycles, and yields one valid sequence. For sequential scripts, this is the simplest interface.

Do not treat the relative position of independent nodes as a business guarantee. If two tasks do not depend on one another, either may appear first and the result can still be correct.

Building a graph with add

You can also construct the graph incrementally:

from graphlib import TopologicalSorter

ts = TopologicalSorter()
ts.add("download")
ts.add("validate", "download")
ts.add("transform", "validate")
ts.add("publish", "transform")

print(tuple(ts.static_order()))

add(node, *predecessors) accumulates dependencies. Calling add again for the same node adds more predecessors. This is useful when plugins, modules, or configuration files contribute separate pieces of one workflow.

Running tasks in parallel

The incremental API is designed for concurrent execution. First call prepare(). Then get_ready() returns every node whose dependencies are satisfied. When a worker finishes, call done() with that task. More nodes may then become ready.

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

requirements = {
    "extract_customers": set(),
    "extract_orders": set(),
    "join": {"extract_customers", "extract_orders"},
    "report": {"join"},
}

def run(name):
    print("running", name)
    return name

ts = TopologicalSorter(requirements)
ts.prepare()

with ThreadPoolExecutor(max_workers=4) as pool:
    futures = {}
    while ts.is_active() or futures:
        for task in ts.get_ready():
            futures[pool.submit(run, task)] = task

        if not futures:
            break

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

This pattern allows parallelism only among ready tasks. Calling future.result() also propagates worker exceptions. In a production system, define the failure policy explicitly: stop the entire graph, retry the failed task, block descendants, or perform compensation.

prepare, get_ready, done, and is_active

prepare() finalizes graph preparation and detects cycles. Once execution begins, the graph should be treated as fixed. get_ready() may return several nodes, and every returned node should later be reported through done() after successful completion.

Never mark a task done before its real work finishes. That releases dependent nodes too early and turns a correct dependency graph into an incorrect execution. Do not call done() twice for the same node either.

is_active() tells you whether useful work remains or tasks are still outstanding. It is convenient for controlling scheduler loops, especially when tasks are sent to an external queue.

Detecting cycles

from graphlib import TopologicalSorter, CycleError

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

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

Cycles usually indicate a modeling or configuration mistake. The exception may expose nodes involved in the cycle, but critical logic should not depend on one exact error representation. A better diagnostic records the loaded dependency edges and shows users a readable chain.

Normalizing configuration

Dependencies loaded from YAML, JSON, or a database may contain duplicate names, missing nodes, and empty strings. Normalize input before creating the sorter.

def normalize(config):
    graph = {}
    for name, dependencies in config.items():
        name = name.strip()
        if not name:
            raise ValueError("task without a name")
        graph[name] = {d.strip() for d in dependencies if d.strip()}
    return graph

TopologicalSorter accepts predecessors that never appear as dictionary keys and automatically includes them as nodes. This can be convenient, but it may hide a typo. Strict systems should verify every dependency against a known task catalog.

A file pipeline example

from graphlib import TopologicalSorter

pipeline = {
    "archive": {"make_csv", "make_pdf"},
    "send": {"archive"},
    "make_csv": {"query"},
    "make_pdf": {"query", "load_template"},
    "query": set(),
    "load_template": set(),
}

for step in TopologicalSorter(pipeline).static_order():
    print(step)

query and load_template can run first, possibly in parallel. make_pdf waits for both, while make_csv depends only on the query. archive waits for both generated artifacts, and send finishes the flow.

Managing outputs

The sorter organizes nodes but does not store task results. Keep a result dictionary or an external store:

results = {}

def execute(task):
    if task == "query":
        results[task] = [1, 2, 3]
    elif task == "make_csv":
        results[task] = create_csv(results["query"])

With threads, protect shared mutable structures when writes may overlap. With processes or distributed workers, use a database, cache, message broker, or object store. Associate outputs with a run identifier so concurrent executions cannot mix data.

Idempotency and recovery

A reliable orchestrator must survive restarts. Make tasks idempotent whenever possible: running a task again should produce the same final state. Record states such as pending, running, completed, and failed. On recovery, rebuild the graph and consider a node complete only after its output has been verified.

TopologicalSorter does not persist state and is not a replacement for Airflow, Prefect, Celery, or a dedicated workflow engine. It is a lightweight building block for applications that need local dependency control or want to implement a custom scheduler.

Retries and failure propagation

Retries should belong to the task-execution layer, not to the dependency graph itself. Limit retry counts, use exponential backoff for transient failures, and avoid retrying permanent validation errors. Descendants of a failed node should remain blocked unless the failure is explicitly ignored or an alternative dependency path exists.

Store the original exception, attempt count, start time, and finish time. These records make incidents understandable and help distinguish slow dependencies from actual scheduler problems.

Memory and scale

The graph is kept in memory. Thousands or tens of thousands of nodes may be perfectly reasonable, but measure your workload. Huge dynamic graphs may require partitioning or a specialized system. Topological sorting also does not reduce the cost of the tasks; it only determines when each one is allowed to start.

Prefer immutable hashable node identifiers such as strings, integers, enums, or tuples. Nodes are used as dictionary and set members, so mutable values are unsuitable.

Testing dependency graphs

Test more than the happy path. Include independent branches, a shared prerequisite, a node with several predecessors, an unknown dependency, and at least one deliberate cycle. For concurrent execution, verify that descendants never start before all predecessors have completed.

A useful unit test checks relationships instead of one exact total order. For every edge dependency -> task, assert that the dependency index is lower than the task index. This keeps tests correct even when independent nodes change positions.

Best practices

Use stable names, validate unknown dependencies, detect cycles before expensive work starts, limit worker concurrency, propagate failures, record durations and outputs, and never confuse “ready” with “completed.” Important workflows should persist execution events and include correlation identifiers in logs.

Related Academify guides include Python dictionaries, Python sets, Python threading, and Python concurrent.futures. External references include the official graphlib documentation and the official concurrent.futures documentation.

Conclusion

graphlib.TopologicalSorter turns a network of dependencies into an executable order and provides a safe foundation for controlled parallelism. For small and medium workflows, it replaces manual sequencing, detects cycles, and makes task contracts explicit. Combined with validation, persistence, idempotency, and careful failure handling, it becomes a practical component for building reliable pipelines with Python’s standard library.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Folders and directories representing Python os.fwalk
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python os.fwalk: Traverse Directories

    Learn Python os.fwalk to traverse directories with file descriptors, reduce race conditions, and handle files more safely.

    Ler mais

    Tempo de leitura: 4 minutos
    05/09/2026
    Software developer reviewing Python code and overridden methods
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    typing.override: Validate Method Overrides

    Learn Python typing.override for safer method overrides, compatible signatures, inheritance contracts, and reliable refactoring.

    Ler mais

    Tempo de leitura: 6 minutos
    05/09/2026
    Software developer working with Python queues and threads
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    queue.SimpleQueue: Thread-Safe FIFO Queue

    Learn Python queue.SimpleQueue for thread-safe FIFO queues, worker pipelines, clean shutdown, and reliable producer-consumer designs.

    Ler mais

    Tempo de leitura: 5 minutos
    04/09/2026
    Software developer working with Python enums and API code
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python StrEnum: String Enums

    Learn Python StrEnum for string-based enums, input validation, JSON serialization, APIs, configuration, and cleaner domain contracts.

    Ler mais

    Tempo de leitura: 4 minutos
    04/09/2026
    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