Python graphlib: Topological Task Order

Published on: August 27, 2026
Reading time: 6 minutes
A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.

The graphlib module provides TopologicalSorter, a class for ordering tasks that have dependencies. A topological order arranges nodes in a directed acyclic graph so every prerequisite appears before the item that depends on it. This pattern appears in build systems, data pipelines, migrations, package installation, report generation, and job orchestration.

The module is not a general graph library. It does not calculate shortest paths, centrality, components, or flows. Its focus is dependency ordering and an incremental API that allows several ready nodes to run in parallel.

The dependency model

TopologicalSorter accepts a mapping where each key is a node and the value is an iterable containing that node’s predecessors.

from graphlib import TopologicalSorter

graph = {
    "test": {"install"},
    "package": {"test"},
    "publish": {"package"},
    "install": set(),
}

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

The relationship means “this task depends on these predecessors.” Reversing that interpretation is a common source of incorrect schedules.

Nodes must be hashable

Nodes are stored in dictionaries and sets, so they must be hashable. Strings, numbers, enums, and immutable tuples are natural choices.

Do not use mutable objects with unstable hashes. Keep a compact immutable task ID in the graph and store detailed metadata in a separate mapping.

Implicit predecessor nodes

A predecessor mentioned in a value does not have to appear as an explicit key. The sorter adds it as a node with no known predecessors.

graph = {
    "deploy": {"build"},
}

Here build becomes part of the graph. The convenience is useful, but declaring every node explicitly improves validation and documentation.

static_order

static_order() is the simplest way to produce a complete valid order.

sorter = TopologicalSorter(graph)
for task in sorter.static_order():
    run(task)

The result is an iterator. Consume it once or convert it to a tuple when the sequence must be reused.

Several orders may be valid

When two tasks are independent, either relative order can satisfy the graph. Tests should not demand one exact total sequence unless the application adds another deterministic rule.

Validate precedence instead: every predecessor must appear before its dependent. Sort ready nodes by a stable key when deterministic presentation is required.

Cycle detection

A topological order exists only for an acyclic graph. A dependency cycle raises CycleError.

from graphlib import CycleError, TopologicalSorter

try:
    order = tuple(TopologicalSorter({
        "a": {"b"},
        "b": {"a"},
    }).static_order())
except CycleError as error:
    print("cycle detected", error)

A cycle means none of its tasks can begin because each waits for another member of the same group.

Reporting cycles

The exception contains information that helps identify a cycle, but it should not be treated as a complete user-facing reporting API. Convert task IDs into readable names and display a clear chain.

A graph may contain several cycles. Fixing the first one can reveal another, so validate again after every correction.

Add nodes incrementally

add(node, *predecessors) adds or extends dependencies before preparation.

sorter = TopologicalSorter()
sorter.add("compile", "generate_code")
sorter.add("test", "compile")
sorter.add("publish", "test")

Repeated calls for one node merge predecessor sets. This is useful when plugins contribute dependency edges.

prepare

The incremental execution API begins with prepare(). It validates the graph and enables get_ready() and done().

sorter.prepare()

Do not add new dependencies after the graph has been prepared. Construct and validate the entire configuration first.

get_ready

get_ready() returns nodes whose predecessors have already been marked complete.

ready = sorter.get_ready()
for task in ready:
    start(task)

Returned nodes become “in progress.” They are not returned again, so the coordinator must call done() when they finish.

done

done(*nodes) marks previously ready nodes as complete.

sorter.done("generate_code")
new_ready = sorter.get_ready()

Do not mark a node that was never returned by get_ready(). Invalid state transitions raise errors.

is_active

is_active() reports whether progress remains possible: either ready nodes have not been retrieved or in-progress nodes may still complete.

while sorter.is_active():
    for task in sorter.get_ready():
        run(task)
        sorter.done(task)

In parallel execution, completed tasks normally arrive through a result queue or future set.

Parallel execution

The incremental API can submit every ready node to workers, wait for completions, and release dependent nodes.

from concurrent.futures import ThreadPoolExecutor, wait, FIRST_COMPLETED

sorter.prepare()
active = {}

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

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

A production version also needs failure, cancellation, and shutdown policies.

Task failures

A failed task should not automatically be marked complete when its dependents require a valid result. The coordinator must choose whether to stop the pipeline, mark descendants blocked, or use a documented fallback.

Keep distinct states such as succeeded, failed, cancelled, skipped, and blocked.

Cancellation

During cancellation, stop submitting new nodes, request cooperative worker cancellation, and wait for a deadline. Do not call done() on incomplete tasks merely to empty the sorter.

Optional paths should be modeled explicitly outside the mandatory dependency graph.

Optional dependencies

TopologicalSorter represents required precedence. Feature flags, environment conditions, and optional plugins need a graph-building phase that creates the effective graph before prepare().

Remove disabled nodes and rewrite their edges before execution begins.

Validate references

Because missing predecessor keys become implicit nodes, a typo can silently create a phantom task.

declared = set(configuration)
referenced = set().union(*configuration.values())
unknown = referenced - declared

Decide whether implicit nodes are allowed. User-configured pipelines often benefit from rejecting unknown names.

Separate identity from execution

Keep task IDs in the graph and functions in another mapping.

tasks = {
    "extract": lambda: extract(data),
    "transform": transform,
    "load": load,
}

This keeps the graph serializable, improves logs, and prevents validation from executing code.

Side effects and idempotency

A valid order does not protect against partial writes. Tasks that update files, databases, or APIs need idempotency keys, transactions, and rollback plans.

When resuming a pipeline, do not assume that a logged start means successful completion. Persist checkpoints only after a verifiable result.

Priorities

The module does not prioritize simultaneously ready nodes. The coordinator may sort the tuple returned by get_ready().

for task in sorted(sorter.get_ready(), key=priority):
    start(task)

Priority can choose among released nodes but cannot violate dependencies.

Concurrency limits

Not every ready task should start immediately. Respect worker counts, database pool capacity, memory, network bandwidth, and rate limits.

Maintain a local ready queue or submit only up to available capacity. See Python concurrent.futures for backpressure and executor design.

Resource classes

Some tasks consume CPU, others use databases or networks. One global worker limit may be inappropriate. Use separate executors or semaphores by resource type.

The graph describes precedence; the scheduler describes capacity.

Persistent workflow state

TopologicalSorter is not a durable workflow engine. A process restart loses in-memory progress.

Critical pipelines should persist the graph, attempts, outputs, timestamps, and status in transactional storage. Rebuild a sorter only to determine which tasks may proceed.

Large graphs

Topological sorting normally scales with the number of nodes and edges, but representation and memory still matter. Avoid unnecessary duplicate structures.

Use compact IDs and enforce limits before accepting external configurations.

Untrusted graph input

A user can submit a graph with huge identifiers, millions of edges, or extreme fan-in and consume CPU and memory.

Limit node count, edge count, identifier length, and preparation time. Never map task names directly to imports or shell commands without an allowlist.

Visualization

The module does not generate diagrams. Export edges to DOT, JSON, or a table for debugging. Highlight cycles, blocked tasks, and leaf nodes.

Visualization becomes especially valuable when plugins add dependencies automatically.

Testing

Test an empty graph, one node, a linear chain, independent branches, convergence, multiple valid orders, cycles, unknown references, task failure, cancellation, and parallel completion.

When order is not unique, assert precedence constraints rather than an exact list.

Common mistakes

Common failures include reversing the dependency direction, requiring one exact order, forgetting done(), marking failed tasks complete, adding nodes after preparation, accepting typos as implicit tasks, launching unlimited work, and treating the sorter as a persistent workflow system.

Conclusion

graphlib.TopologicalSorter provides a direct solution for dependency ordering and an incremental interface suitable for parallel schedulers. Model predecessors correctly, validate cycles and names, cap concurrency, and keep execution state separate from graph structure.

Consult the official graphlib documentation and Python multiprocessing when workers require separate processes.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Python asynchronous code on a laptop for inspect.markcoroutinefunction
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    markcoroutinefunction: Detect Async Wrappers

    Learn inspect.markcoroutinefunction in Python to identify async wrappers, integrate frameworks, and avoid incorrect coroutine detection.

    Ler mais

    Tempo de leitura: 5 minutos
    10/10/2026
    Python code for traversing folders and files with Path.walk
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Path.walk: Traverse Directories Safely

    Learn Python Path.walk to traverse directories, filter files, handle errors, and control directory-tree processing safely.

    Ler mais

    Tempo de leitura: 5 minutos
    10/10/2026
    Python process debugging in a terminal with source code
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pdb -p: Debug Running Python Processes

    Learn how to attach pdb to a running Python process, inspect stacks, and diagnose hangs safely.

    Ler mais

    Tempo de leitura: 6 minutos
    09/10/2026
    Python programming code for exact fractions
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    fractions.from_number: Convert Numbers to Fractions

    Learn Python fractions.from_number for exact rational conversion, float and Decimal handling, approximation, validation, and safe arithmetic.

    Ler mais

    Tempo de leitura: 4 minutos
    09/10/2026
    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026