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

    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    weakref: Avoid Retaining Objects in Caches

    Learn Python weakref for weak references, caches, WeakSet, WeakMethod, finalize callbacks, and avoiding accidental object retention.

    Ler mais

    Tempo de leitura: 7 minutos
    27/08/2026
    Red leader figure connected to wooden figures, emphasizing leadership and teamwork.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python contextlib: Manage Resources Safely

    Learn Python contextlib with contextmanager, ExitStack, suppress, closing, asynccontextmanager, and safe resource cleanup.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ast: Analyze and Transform Code

    Learn Python ast to analyze and transform source code, build visitors, preserve positions, use literal_eval, and avoid execution risks.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026
    Rustic exposed brick wall featuring aged electrical sockets and metal conduit.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python socket: Build TCP and UDP Networks

    Learn Python socket for TCP and UDP clients and servers, framing, timeouts, IPv6, concurrency, TLS, and network security.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026
    Hands typing code on a laptop in a workspace. Indoor setting focused on software development.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python multiprocessing: Use Multiple Cores

    Learn Python multiprocessing with processes, pools, queues, pipes, shared memory, cancellation, security, and correct shutdown.

    Ler mais

    Tempo de leitura: 7 minutos
    26/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    concurrent.futures: Threads and Processes in Parallel

    Learn Python concurrent.futures with threads, processes, Future objects, timeouts, cancellation, backpressure, and deadlock prevention.

    Ler mais

    Tempo de leitura: 6 minutos
    26/08/2026