Step pipelines with dependencies
Real workflows aren't a flat list of independent tasks — the steps depend on each other. You can't deploy before the build passes; you can't write the report before the data loads; transform needs fetch's output. The order matters, and each step feeds the next.
A step pipeline models this. Each step has a name and a function. The engine runs them in order, passing each step's output as the input to the next — the data flows down the chain. The first step gets the initial input; the last step's output is the final result.
The key idea is chaining: a current value that each step transforms in turn.
````
current = initial
for step in pipeline:
current = step['fn'](current) # output becomes next input
For correctness you also want to validate dependencies: if a step declares depends_on=['load'], then load must appear *earlier* in the pipeline. A dependency that's missing entirely, or that comes *later* (a forward reference), is a broken pipeline — catch it before you run, not halfway through.
The check is a running seen-set: walk the steps in order, and a step's dependencies must all already be in seen. Add the step's own name to seen only *after* checking it — that's what catches forward references.
Syntax
# Chain outputs through the steps
current = initial_input
for step in pipeline:
current = step['fn'](current) # previous output → next input
# Validate dependency order
seen = set()
for step in pipeline:
for dep in step['depends_on']:
if dep not in seen: # missing or forward-referenced
return False
seen.add(step['name']) # add AFTER checkingWorked examples
Each step's output feeds the next
def add_step(pipeline, name, fn):
pipeline.append({'name': name, 'fn': fn, 'depends_on': []})
def run_pipeline(pipeline, initial_input):
current = initial_input
steps_run = []
for step in pipeline:
current = step['fn'](current)
steps_run.append(step['name'])
return {'steps_run': steps_run, 'final_output': current}
p = []
add_step(p, 'double', lambda x: x * 2)
add_step(p, 'add_ten', lambda x: x + 10)
print(run_pipeline(p, 5))
# Output:
# {'steps_run': ['double', 'add_ten'], 'final_output': 20}A realistic load → transform → summarise chain
def run_pipeline(steps, initial):
current = initial
for fn in steps:
current = fn(current)
return current
load = lambda src: [10, 20, 30]
transform = lambda rows: [r + 1 for r in rows]
summarise = lambda rows: {'count': len(rows), 'total': sum(rows)}
print(run_pipeline([load, transform, summarise], 'db://prod'))
# Output:
# {'count': 3, 'total': 63}Validating dependency order
def validate(pipeline):
seen = set()
for step in pipeline:
for dep in step['depends_on']:
if dep not in seen:
return False
seen.add(step['name'])
return True
def s(name, deps):
return {'name': name, 'depends_on': deps}
good = [s('load', []), s('transform', ['load']), s('export', ['load', 'transform'])]
bad = [s('transform', ['load']), s('load', [])] # forward reference
print(validate(good)) # True
print(validate(bad)) # False
# Output:
# True
# FalseCommon mistakes & gotchas
A step that returns None breaks the chain
If a step forgets to return (or returns None), the *next* step receives None as its input and usually crashes with a TypeError or AttributeError. Every step in a chained pipeline must return the value it wants to pass on.
Adding the name to seen before checking deps
In validate_dependencies, if you seen.add(step['name']) *before* checking that step's own depends_on, a step could 'satisfy' its own dependency and forward references slip through. Check the deps first, add the name after.
Order of registration is the order of execution
This simple engine runs steps in the order they were added — it does not topologically sort by depends_on. depends_on only *validates* the order you declared; it doesn't reorder for you. Add the steps in dependency order yourself, and use validate to confirm you got it right.
Why it matters
ETL jobs, CI/CD pipelines, deploy scripts, data workflows — they're all chains of dependent steps where the output of one is the input of the next. Modelling that explicitly, with the order validated up front, is what separates a reliable pipeline from a pile of scripts you have to remember to run in the right order by hand.