dspy.Flex¶
Flex is a DSPy module whose implementation is optimizable code rather than a fixed prompt. You construct it from a signature, and it defaults to a thin baseline over that signature. What makes it different is what an optimizer is allowed to do with it: instead of only rewriting instructions, dspy.GEPA can rewrite the module’s entire source — splitting the task into multiple predictors, folding deterministic steps into plain Python, and authoring its own helper functions. Being a Flex is what tells GEPA that the module’s code is an optimizable parameter.
When to Use Flex¶
Reach for Flex when you’d rather have the optimizer discover the program’s structure than hand-write it. That’s the case when:
- The best decomposition is unknown or worth searching — you have a metric and a dataset to judge candidate structures against.
- Parts of the task are deterministic and shouldn’t cost an LM call — arithmetic, parsing, lookups, normalization.
- You want the optimizer to trade accuracy against cost — e.g. rewarding programs that answer clear cases in code and reserve the LM for genuinely hard ones.
Basic Usage¶
import dspy
dspy.configure(lm=dspy.LM("openai/gpt-5"))
# Construct Flex from a signature, like any module.
solve = dspy.Flex("invoice: str -> total_cents: int")
# Runs the baseline (a single dspy.Predict).
result = solve(invoice="2 widgets @ $3.50, shipping $1.00")
print(result.total_cents)
Out of the box, solve is just a dspy.Predict over the signature, wrapped in a module (with tools, it starts as a dspy.RLM instead — see Tools). The point of Flex is what happens when you optimize it (see Optimizing with GEPA): GEPA can replace that baseline with, say, a predictor that only extracts quantities and unit prices, and a line of Python that multiplies and sums them.
The generated code always runs in a sandbox (interpreter_factory defaults to dspy.PythonInterpreter), so the example above needs Deno installed — see Sandboxed Execution.
How Optimization Works¶
dspy.GEPA discovers Flex submodules by type. When GEPA compiles a program containing one or more Flex submodules, it treats each one as a code component: rather than proposing a new instruction string, its reflection model proposes a new whole module source, guided by the signature, any available tools, and your metric’s feedback on failing examples. GEPA binds the candidate source, evaluates it, and keeps it if it advances the Pareto frontier — the same search GEPA runs for prompts, applied to code.
A broken candidate can’t crash the optimization run. If the reflection model emits source that fails to bind, GEPA scores that candidate as a failure and moves on, rather than aborting the optimization.
Optimizing with GEPA¶
You optimize a Flex the same way you optimize any DSPy program — hand it to dspy.GEPA with a metric and a trainset:
import dspy
dspy.configure(lm=dspy.LM("openai/gpt-5-mini")) # runs the program
def metric(gold, pred, trace=None, pred_name=None, pred_trace=None):
correct = getattr(pred, "total_cents", None) == gold.total_cents
fb = "Correct." if correct else (
f"Wrong total: got {getattr(pred, 'total_cents', None)}, expected {gold.total_cents}. "
"Have the LM extract line items, then sum them in Python."
)
return dspy.Prediction(score=1.0 if correct else 0.0, feedback=fb)
solve = dspy.Flex("invoice: str -> total_cents: int")
optimized = dspy.GEPA(
metric=metric,
reflection_lm=dspy.LM("openai/gpt-5", temperature=1.0, max_tokens=8000),
max_metric_calls=60,
).compile(solve, trainset=trainset, valset=valset)
print(optimized.module_src) # the discovered program
The metric returns a dspy.Prediction(score=..., feedback=...) — a scalar plus natural-language feedback that GEPA reflects on to revise the module. For how to write an effective feedback metric, see Implementing Feedback Metrics in the GEPA guide and the dspy.GEPA tutorials.
Rewarding leaner programs with a trace-aware metric¶
A common goal with Flex is to push work out of the LM and into deterministic code. To optimize for that, your metric needs to see how an answer was produced, not just whether it was right. Declare a program_trace parameter and GEPA will pass the execution trace to the metric at scoring time, letting you penalize LM calls:
LLM_CALL_PENALTY = 0.15
def metric(gold, pred, trace=None, pred_name=None, pred_trace=None, program_trace=None):
correct = getattr(pred, "total_cents", None) == gold.total_cents
n_calls = len(program_trace) if program_trace else 0
score = max(0.0, (1.0 if correct else 0.0) - LLM_CALL_PENALTY * n_calls)
fb = f"{'Correct' if correct else 'Wrong'} — used {n_calls} LM call(s). Settle clear cases in Python."
return dspy.Prediction(score=score, feedback=fb)
The program_trace parameter is opt-in by declaration: only metrics that name it receive the trace. Keep the penalty small relative to correctness, so a decomposition has to hold accuracy to win.
Sandboxed Execution¶
Flex always runs its generated code in a sandbox — never in the host Python process. interpreter_factory defaults to dspy.PythonInterpreter (Deno/Pyodide) and must be a zero-argument factory returning a fresh CodeInterpreter; a bare instance is not accepted, so parallel evaluations receive isolated sessions. The factory is called once per sandbox session, including separate sessions requested by nested code-executing modules. The code is authored by the reflection model, so isolating it keeps it from running with your host’s full permissions. With the default interpreter, optimizer-authored control flow, string work, arithmetic, and supported imports run inside the sandbox, and only provided-tool calls, predictor construction, and predictor calls bridge back to the host, which makes the real LM calls.
Because the default builds a PythonInterpreter, running a Flex needs Deno installed; without it, the call raises.
solve = dspy.Flex(
"invoice: str -> total_cents: int",
interpreter_factory=lambda: dspy.PythonInterpreter(), # the default; swap in your own CodeInterpreter factory here
)
Each call owns and shuts down every interpreter session it creates, so a Flex holds no live sessions between calls.
Tools¶
Pass tools and the baseline starts as a dspy.RLM instead of a dspy.Predict.
def lookup_sku(code: str) -> dict:
"""Look up a product by SKU."""
return catalog[code]
solve = dspy.Flex("order: str -> total_cents: int", tools=[lookup_sku])
The optimizer can then wire your tools into dspy.RLM(..., tools=[...]) / dspy.ReAct(..., tools=[...]), or call them directly from forward.
Saving and Loading¶
A Flex serializes its module_src, so saving and loading a program restores the optimized code:
optimized.save("solver.json")
restored = dspy.Flex("invoice: str -> total_cents: int")
restored.load("solver.json") # rebinds the saved module_src
The interpreter is a runtime dependency and is not serialized. Reconstructing with dspy.Flex(signature) restores the default sandbox automatically; if you optimized with a custom interpreter_factory, pass the same one when you reconstruct the module before calling load.
Constructor Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
signature |
str \| Signature |
required | Declares the module’s inputs and outputs (e.g. "invoice -> total_cents: int"). |
tools |
list[Callable \| dspy.Tool] |
None |
Tools the generated code may call. With tools, the baseline is a dspy.RLM; without, a dspy.Predict. |
interpreter_factory |
Callable[[], CodeInterpreter] |
PythonInterpreter |
Zero-arg factory returning a fresh CodeInterpreter for each sandbox session; defaults to dspy.PythonInterpreter (needs Deno). A bare interpreter instance is not accepted. Supported Python and libraries are interpreter-dependent. |
max_predictor_calls |
int \| None |
100 |
Maximum number of predictor calls the generated code can make in one forward — a guard against runaway loops. None removes the limit. |
Notes¶
Experimental
Flex is marked experimental. The API and the optimization behavior may change between releases; pin a version if you depend on it.
Interpreter Requirements
Flex always runs generated code in a sandbox (interpreter_factory defaults to dspy.PythonInterpreter), which requires Deno for its Pyodide WASM sandbox — see the RLM page for installation notes.
API Reference¶
dspy.Flex(signature: Any, *, tools: list[Any] | None = None, interpreter_factory: Callable[[], CodeInterpreter] = PythonInterpreter, max_predictor_calls: int | None = 100)
¶
Bases: Module, Parameter
A module whose implementation is optimizable code, not just a prompt.
Construct it like any module (dspy.Flex(MySignature)). It starts as a baseline that delegates
to a single dspy.Predict over the signature — or dspy.RLM when tools are given, so the
baseline can call them. dspy.GEPA recognizes Flex instances by type and rewrites their
source — a single dspy.Module subclass, exposed as module_src — into decomposed predictors
plus plain Python instead of only tuning instructions.
The optimizer-authored code runs inside an interpreter. Flex never
runs it in the host Python process. interpreter_factory defaults to dspy.PythonInterpreter
(Deno/Pyodide) and must be a zero-argument callable returning a new CodeInterpreter.
Flex may validate or lower source and install its guest shim before execution; a custom
interpreter therefore defines the Python and standard-library subset available to that source.
The optimizer-authored glue runs isolated; only provided-tool calls, predictor construction,
and predictor calls bridge back to the host, which makes the real LM calls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signature
|
Any
|
A |
required |
tools
|
list[Any] | None
|
|
None
|
interpreter_factory
|
Callable[[], CodeInterpreter]
|
Zero-argument callable returning a fresh |
PythonInterpreter
|
max_predictor_calls
|
int | None
|
Maximum number of predictor calls the optimizer-authored code can
make per |
100
|
Source code in .venv/lib/python3.14/site-packages/dspy/predict/flex/flex.py
Attributes¶
module_src: str | None
property
¶
signature: type[Signature]
property
¶
Methods:¶
__call__(*args, **kwargs) -> Prediction
¶
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/module.py
forward(*args: Any, **kwargs: Any) -> Any
¶
Run the bound forward inside the interpreter.
Source code in .venv/lib/python3.14/site-packages/dspy/predict/flex/flex.py
deepcopy()
¶
Deep copy the module.
This is a tweak to the default python deepcopy that only deep copies self.parameters(), and for other
attributes, we just do the shallow copy.
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/base_module.py
dump_state(json_mode: bool = True) -> dict[str, Any]
¶
get_lm()
¶
inspect_history(n: int = 1, file: TextIO | None = None) -> None
¶
Display the LM call history for this module.
Prints a formatted view of the most recent language model calls made by this module, useful for debugging and understanding the module’s behavior.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
The number of recent history entries to display. Defaults to 1. |
1
|
file
|
TextIO | None
|
An optional file-like object to write output to. When
provided, ANSI color codes are automatically disabled.
Defaults to |
None
|
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/module.py
load(path, allow_pickle=False, allow_unsafe_lm_state=False)
¶
Load the saved module. You may also want to check out dspy.load, if you want to load an entire program, not just the state for an existing program.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Path to the saved state file, which should be a .json or a .pkl file |
required |
allow_pickle
|
bool
|
If True, allow loading .pkl files, which can run arbitrary code. This is dangerous and should only be used if you are sure about the source of the file and in a trusted environment. |
False
|
allow_unsafe_lm_state
|
bool
|
If True, preserves unsafe LM endpoint keys (e.g.,
|
False
|
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/base_module.py
load_state(state: dict[str, Any], *, allow_unsafe_lm_state: bool = False) -> None
¶
Source code in .venv/lib/python3.14/site-packages/dspy/predict/flex/flex.py
named_parameters()
¶
Unlike PyTorch, handles (non-recursive) lists of parameters too.
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/base_module.py
named_predictors()
¶
named_sub_modules(type_=None, skip_compiled=False) -> Generator[tuple[str, BaseModule], None, None]
¶
Find all sub-modules in the module, as well as their names.
Say self.children[4]['key'].sub_module is a sub-module. Then the name will be
children[4]['key'].sub_module. But if the sub-module is accessible at different
paths, only one of the paths will be returned.
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/base_module.py
parameters()
¶
predictors()
¶
Return all Predict modules in this module.
Returns:
| Type | Description |
|---|---|
|
list[Predict]: A list of all Predict instances in this module. |
Examples:
>>> import dspy
>>> class MyProgram(dspy.Module):
... def __init__(self):
... super().__init__()
... self.qa = dspy.Predict("question -> answer")
...
>>> program = MyProgram()
>>> len(program.predictors())
1
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/module.py
reset() -> None
¶
Clear the LM; module_src is kept, and predictors are rebuilt from it each forward.
reset_copy()
¶
Deep copy the module and reset all parameters.
save(path, save_program=False, modules_to_serialize=None)
¶
Save the module.
Save the module to a directory or a file. There are two modes:
- save_program=False: Save only the state of the module to a json or pickle file, based on the value of
the file extension.
- save_program=True: Save the whole module to a directory via cloudpickle, which contains both the state and
architecture of the model.
If save_program=True and modules_to_serialize are provided, it will register those modules for serialization
with cloudpickle’s register_pickle_by_value. This causes cloudpickle to serialize the module by value rather
than by reference, ensuring the module is fully preserved along with the saved program. This is useful
when you have custom modules that need to be serialized alongside your program. If None, then no modules
will be registered for serialization.
We also save the dependency versions, so that the loaded model can check if there is a version mismatch on critical dependencies or DSPy version.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Path to the saved state file, which should be a .json or .pkl file when |
required |
save_program
|
bool
|
If True, save the whole module to a directory via cloudpickle, otherwise only save the state. |
False
|
modules_to_serialize
|
list
|
A list of modules to serialize with cloudpickle’s |
None
|
Source code in .venv/lib/python3.14/site-packages/dspy/primitives/base_module.py
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | |