Build multi-step DSPy programs as composable, optimizable modules using dspy.Module and forward(). Use when composing Predict and ChainOfThought calls into a reusable pipeline, structuring a RAG pipeline as a class, creating nested DSPy modules, or designing a class-based multi-step program. Also used for dspy.Module, forward() method, custom DSPy module, compose DSPy calls, multi-step DSPy program, pipeline as a class, reusable AI components, nested DSPy modules, module design patterns, how to structure a DSPy program, class-based DSPy pipeline, self.predict in forward, modular AI pipeline, build complex DSPy programs, combine multiple DSPy calls into one module, save and load optimized module state.
Build multi-step DSPy programs as composable, optimizable modules using dspy.Module and forward(). Use when composing Predict and ChainOfThought calls into a reusable pipeline, structuring a RAG pipeline as a class, creating nested DSPy modules, or designing a class-based multi-step program. Also used for dspy.Module, forward() method, custom DSPy module, compose DSPy calls, multi-step DSPy program, pipeline as a class, reusable AI components, nested DSPy modules, module design patterns, how to structure a DSPy program, class-based DSPy pipeline, self.predict in forward, modular AI pipeline, build complex DSPy programs, combine multiple DSPy calls into one module, save and load optimized module state.
Build Composable AI Programs with dspy.Module
Guide the user through structuring DSPy programs as reusable, composable modules. A dspy.Module is the building block for all DSPy programs -- like PyTorch's nn.Module but for language model pipelines.
Step 1 — Gather context
Ask before writing any code:
What kind of pipeline? (RAG, classify-then-route, iterative refinement, simple extraction?)
How many sub-modules / steps? (Single-step can skip the Module wrapper; 2+ steps benefit from it)
Will you optimize this with a DSPy optimizer? (If yes, sub-module layout as self. attributes is critical)
Do you need async support? (Requires async def forward() and acall — impacts design from the start)
Then build or explain the pattern that fits.
What is dspy.Module
dspy.Module is the building block for multi-step DSPy programs. Declare sub-modules in __init__ as self. attributes, wire them together with Python logic in forward(). DSPy optimizers automatically discover and tune all sub-modules in the tree.
Composing modules -- nesting modules within modules
Modules are composable. A module can use other custom modules as sub-modules:
This is useful for verifying your module hierarchy and debugging which sub-modules exist.
Module state -- save and load
After optimization, save the learned state (few-shot demos, instructions) and reload it later:
# Save after optimization
optimized_program = optimizer.compile(my_program, trainset=trainset)
optimized_program.save("my_program.json")
# Load into a fresh instance
loaded = MyProgram()
loaded.load("my_program.json")
# Use the loaded program -- it has the optimized prompts
result = loaded(question="What is DSPy?")
What gets saved:
Few-shot demonstrations discovered by optimizers
Optimized instructions (from MIPROv2, GEPA)
Any state that DSPy's Predict modules track
What does not get saved:
Python logic in forward() -- that's your code
Model weights (unless you used BootstrapFinetune)
The LM configuration -- you must call dspy.configure() before loading
Validated outputs with Refine
Use dspy.Refine to enforce quality constraints on outputs through a reward function. This replaces the older dspy.Assert/dspy.Suggest pattern:
classSafeQA(dspy.Module):
def__init__(self):
self.generate = dspy.ChainOfThought("question -> answer")
defforward(self, question):
returnself.generate(question=question)
defanswer_reward(args, pred):
"""Score answer quality. Returns float between 0.0 and 1.0."""
score = 0.0# Hard requirement -- must provide a substantive answerif pred.answer.strip() and pred.answer != "I don't know":
score += 0.6# Quality preference -- at least 10 wordsiflen(pred.answer.split()) >= 10:
score += 0.4return score
# Wrap with Refine to retry until quality threshold is met
validated_qa = dspy.Refine(
module=SafeQA(),
N=3,
reward_fn=answer_reward,
threshold=0.6, # must at least pass the hard requirement
)
dspy.Refine -- wraps a module, scores each attempt with a reward function, and retries until the threshold is met (up to N attempts). Use for requirements that must be met.
Graduated scores -- return partial scores (0.0 to 1.0) to let Refine pick the best near-miss when no attempt fully succeeds.
dspy.BestOfN -- similar to Refine but without cross-attempt feedback; use when attempts are independent.
For detailed Refine patterns and examples, see /dspy-refine and /dspy-best-of-n.
Common patterns
Conditional logic in forward()
Route to different sub-modules based on intermediate results:
expensive_lm = dspy.LM("openai/gpt-4o") # or "anthropic/claude-sonnet-4-5-20250929", etc.
cheap_lm = dspy.LM("openai/gpt-4o-mini") # or any smaller model
pipeline = MyProgram()
pipeline.classify.set_lm(cheap_lm)
pipeline.generate.set_lm(expensive_lm)
Batch processing
Use batch() to process multiple examples in parallel:
pipeline = MyProgram()
examples = [dspy.Example(question=q).with_inputs("question") for q in questions]
results = pipeline.batch(examples, num_threads=4, timeout=120)
When NOT to use dspy.Module
Skip the class if you only need a single LM call and will never optimize or save it — call dspy.Predict or dspy.ChainOfThought directly:
# Fine for one-off scripts and notebooks
predict = dspy.ChainOfThought("question -> answer")
result = predict(question="What is DSPy?")
Wrap in a Module when you need any of: re-use across the codebase, DSPy optimizer traversal, save()/load() state, batch() parallelism, or programmatic inspection via named_predictors().
Choosing a forward() pattern
Pattern
Use when
Key property
Sequential steps
Each step's output feeds the next
Simplest; fully optimizable
Conditional routing
Different paths based on intermediate result
Cheap classifier directs to specialized handler
Loop / retry
Quality check drives iteration count
max_rounds param prevents infinite loops
dspy.Refine wrapper
Retry is boilerplate — just need a reward function
Delegates retry logic to DSPy
Nested modules
Reusable sub-pipeline across multiple parent modules
Optimizers traverse nested trees automatically
Verification
# Check module tree before optimizing -- catch missing self. attributes earlyprint(pipeline)
# Confirm all Predict instances are discoverableprint(pipeline.named_predictors()) # should list every sub-module# Smoke-test on one example before running batch
result = pipeline(**example.inputs())
asserthasattr(result, "answer"), "forward() must return a Prediction with expected fields"
Gotchas
Claude stores sub-modules in a plain list instead of as self. attributes. Optimizers discover sub-modules by traversing self. attributes in __init__. A Predict stored in a local variable or a plain list is invisible to optimization. Use a dict assigned to self. — DSPy traverses dicts for parameters.
Claude puts dspy.configure() inside forward(). Configure once at startup. Calling it per-forward adds overhead and causes unexpected behavior during optimization.
Claude names forward() args differently from training example fields. When an optimizer traces your module, it passes inputs from training examples to forward(). Mismatched argument names cause silent failures. Use the same field names as your dspy.Example inputs.
Claude creates a module with no forward() method. Every dspy.Module subclass must implement forward(). Without it, calling the module raises an error.
Cross-references
Install any skill: npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill <name>
Signatures define inputs and outputs for each sub-module -- see /dspy-signatures
Predict is the simplest sub-module for direct LM calls -- see /dspy-predict
ChainOfThought adds step-by-step reasoning -- see /dspy-chain-of-thought
Multi-step pipelines with real-world patterns -- see /ai-building-pipelines
Optimizing modules to improve accuracy -- see /ai-improving-accuracy
Install /ai-do if you do not have it — it routes any AI problem to the right skill and is the fastest way to work: npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill ai-do