Training Pipeline Orchestration Mock DAG in Python

Build a mock DAG orchestrator that runs ML pipeline stages in dependency order using topological sorting (Kahn's algorithm).

Medium Python 3.10+ Aug 9, 2026 ML engineering pipelines 13 views 0 copies

Python code

58 lines
Python 3.10+
from collections import deque
from dataclasses import dataclass, field


@dataclass
class DAGNode:
    name: str
    task: callable
    dependencies: list[str] = field(default_factory=list)


class MockDAG:
    def __init__(self, nodes: list[DAGNode]):
        self.nodes = {n.name: n for n in nodes}
        self.executed: list[str] = []

    def run(self):
        # Build adjacency and in-degree from dependencies
        adjacency = {name: [] for name in self.nodes}
        in_degree = {name: 0 for name in self.nodes}
        for name, node in self.nodes.items():
            for dep in node.dependencies:
                adjacency[dep].append(name)
                in_degree[name] += 1

        # Topological sort via Kahn's algorithm
        queue = deque([n for n, d in in_degree.items() if d == 0])
        while queue:
            current = queue.popleft()
            node = self.nodes[current]
            node.task()
            self.executed.append(node.name)
            for neighbor in adjacency[current]:
                in_degree[neighbor] -= 1
                if in_degree[neighbor] == 0:
                    queue.append(neighbor)

        if len(self.executed) != len(self.nodes):
            raise RuntimeError("DAG contains cycles")


def main():
    # Example: training pipeline with data loading → preprocessing → training → evaluation
    dag = MockDAG(nodes=[
        DAGNode(name="load_data", task=lambda: print("Loading data")),
        DAGNode(name="preprocess", task=lambda: print("Preprocessing"),
                dependencies=["load_data"]),
        DAGNode(name="train", task=lambda: print("Training model"),
                dependencies=["preprocess"]),
        DAGNode(name="evaluate", task=lambda: print("Evaluating"),
                dependencies=["train"]),
    ])
    dag.run()
    print("Execution order:", dag.executed)


if __name__ == "__main__":
    main()

Output

stdout
Loading data
Preprocessing
Training model
Evaluating
Execution order: ['load_data', 'preprocess', 'train', 'evaluate']

How it works

The DAGNode dataclass captures each pipeline stage with its name, callable, and declared dependencies. The MockDAG.run() method builds an adjacency list and computes in-degrees, then repeatedly executes nodes with zero in-degree (ready nodes) via a queue. Each executed node decrements the in-degree of its downstream dependents, making them eligible when all prerequisites complete. The while loop guarantees that every node runs exactly once in valid topological order, and the cycle check catches malformed pipelines that would deadlock.

Common mistakes

  • Forgetting that dependencies list must reference existing node names, causing KeyError at build time.
  • Assuming insertion order equals dependency order — the graph must be traversed, not listed.
  • Overlooking cycle handling; a circular dependency will raise RuntimeError but only after consuming all queue items.

Variations

  1. Use graphlib.TopologicalSorter from the standard library to replace the custom Kahn implementation.
  2. Add parallel execution of independent nodes using concurrent.futures.ThreadPoolExecutor.

Real-world use cases

  • Orchestrating batch ML jobs where feature extraction, model training, and evaluation must respect prerequisite stage order.
  • Powering experiment pipelines that fan out data transforms and fan back in for aggregate metric computation.
  • Scheduling cascading ETL steps in a regression-test harness to validate new data flows before deployment.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from ML engineering pipelines

Related tutorials and quizzes for this topic.