Composing with pipes
The pipe operator is one method: a step that knows how to call another step. Implement it once and the rest of the framework becomes obvious.
One method, not a language feature
prompt | model | parser looks like syntax, but Python has no pipe for functions. It is prompt.__or__(model), called by the interpreter, returning a new object that represents the two steps joined. Chain three and you get a nested pair whose invoke runs the left side, then the right.
That is worth internalising because it explains the error messages. A pipe that fails usually failed inside one of its steps, and the traceback names the step, not the pipeline. Reading it from the inside out is the fastest way to the bug.
The evaluation order is left to right, so prompt | model | parser builds a pair from the first two, then pairs that with the parser. Each __or__ returns a new object; nothing runs until invoke. That is why you can build a chain at import time and call it many times, and why an exception at build time is a wiring bug rather than a model failure.
Parallel steps and pass-through
Not every arrangement is a straight line. Sometimes one input feeds two steps whose results are merged — a summary and a list of keywords from the same document. RunnableParallel expresses that: run these branches on the same input, collect a dictionary.
RunnablePassthrough covers the other common shape: a branch that needs the original input alongside a computed value. Without it you end up wrapping the input in a tuple just to carry it through, which makes the chain harder to read than the code it replaced.
Fan out, then merge
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
analyse = RunnableParallel(
summary=summary_prompt | model | StrOutputParser(),
keywords=keywords_prompt | model | StrOutputParser(),
original=RunnablePassthrough(),
)
result = analyse.invoke("A long document about vector search ...")
print(result.keys()) # summary, keywords, original
print(result["summary"])Build the pipe yourself
Run it, then chain a third step. The `|` operator should keep working without any change to `Pipe` — that is the whole trick.
The pipe is not the program
A long pipe reads like a sentence and debugs like a stack trace. When it has more than four stages, name the intermediate values: build the prompt, call the model, parse the reply, then compose those three in a short chain. The named version is longer and easier to test, which is the trade you want at that size.
A common mistake is adding a stage to fix a problem in an earlier one. The chain grows, the original bug remains, and the new stage hides it. The symptom is a pipeline that works only in the order it was written. Invoke each step alone with the same input; the step that misbehaves is the one to change.
Why the framework bothers
If a pipe were only sequential calls, nobody would need a library for it. The value appears at the edges: the same object can be invoked once, batched over a list, streamed token by token, or awaited asynchronously, without rewriting the steps.
Those four modes are why steps must share a shape. A step that only works when called once with a string cannot be batched or streamed. The interface is narrow on purpose so the runtime can do more with it.
That is also why a step should not reach for global state. A step that reads a module-level variable behaves differently when batched, because the batch shares the process. Keep the input explicit and the runtime can reorder or parallelise without changing the answer. The constraint feels fussy until the first time you stream a chain and watch a cached value leak between requests.
When a chain misbehaves, invoke each step on its own with the same input before blaming the composition. Nine times out of ten the pipe is fine and one step is not.
Readability has a limit
A pipe is clearer than nested calls for three or four steps. At ten, the line becomes a diagram nobody can hold in their head, and naming the intermediate stages is better than extending the pipe. Composition is a tool for expressing structure, not a contest.
The same applies to branching. Two parallel branches are readable; a tree of six is a function that deserves a name and its own tests.
Two answers from one input
The parallel runner already returns a dictionary. Add a third branch that counts the words, and make sure the original text still comes through untouched.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does a | b do in Python?
