What you need to know

  • DSPy makes prompting a compilation problem. You declare what you want as typed Signatures and compose Modules; DSPy's optimisers figure out the how — the exact instructions and few-shot examples — by searching against your data and a metric. The slogan is "programming, not prompting".
  • Optimisers do the tuning you would have done by hand. BootstrapFewShot bootstraps demonstrations from your own data; MIPROv2 jointly optimises instructions and demonstrations. You hand them a training set and a metric and they search.
  • The gains are real but task-specific. Reported improvements over manual prompting sit around 10–40%, but those are benchmark-dependent and must be measured on your eval set. The dependable win is reproducibility and painless model-swapping.
  • It pairs with evals, not against them. DSPy needs a real eval set and a metric before it can do anything. If you have not built a golden set and a judge yet, that is the prerequisite, not the afterthought.

If you have shipped anything that calls an LLM, you know the ritual. You write a prompt. It works in the demo. You tweak a sentence, re-run a handful of cases, eyeball the output, and tweak again. Three weeks later your prompt is a 600-word wall of "You MUST" and "NEVER" and four hand-picked examples nobody remembers the provenance of. Then the provider ships a new model, you swap the string, and the whole fragile edifice behaves differently. You are back to tweaking. This is the treadmill, and a team in Bengaluru or London running multiple LLM stages is on it permanently.

DSPy — Declarative Self-improving Python, from the Stanford NLP group — is the argument that prompt strings should not be hand-written at all. They should be compiled, the same way you would never hand-assemble machine code when you have a compiler. You write the logic of your pipeline in clean Python; DSPy generates and optimises the prompts underneath. This guide is the production view: the mental model, the three pieces of code you actually write, the optimiser you should pick first, the real trade-offs, and an honest read on whether it belongs in your stack yet.

The core idea: declare, compose, compile

DSPy splits an LLM program into three things. Once you see the split, the rest follows.

Signatures — declare the task, not the prompt

A Signature is a typed specification of a single LLM step: what goes in, what comes out, and a one-line description of the job. Crucially it says nothing about phrasing. You are not writing "You are a helpful assistant that..."; you are declaring the shape of the transformation and letting DSPy work out the words.

import dspy

class ClassifySupportTicket(dspy.Signature):
    """Classify an inbound support ticket by urgency and route it."""

    ticket_text: str = dspy.InputField(desc="raw customer message")
    urgency: str = dspy.OutputField(desc="one of: low, medium, high, critical")
    team: str = dspy.OutputField(desc="team to route to, e.g. billing, technical, account")
    rationale: str = dspy.OutputField(desc="one sentence justifying the routing")

That class is the prompt, conceptually. There is no instruction text to hand-tune, no example block to curate. The field names, types and short descriptions are the contract. This is the same instinct behind structured-output prompting patterns — declaring the output shape rather than begging for it in prose — taken one level further, up the whole program.

Modules — compose the strategy

A Module wraps a Signature with a prompting strategy. dspy.Predict is the bare call. dspy.ChainOfThought adds a reasoning step before the answer. dspy.ReAct interleaves reasoning with tool calls for agentic loops. You compose them like any Python objects, and a multi-stage pipeline is just a class with several modules wired together in its forward method.

class TicketRouter(dspy.Module):
    def __init__(self):
        super().__init__()
        # ChainOfThought tells DSPy to reason before producing the fields
        self.classify = dspy.ChainOfThought(ClassifySupportTicket)

    def forward(self, ticket_text: str):
        return self.classify(ticket_text=ticket_text)

router = TicketRouter()
result = router(ticket_text="My card was charged twice for the London branch order.")
print(result.urgency, result.team, result.rationale)

Swap ChainOfThought for Predict and the strategy changes with no other edits. That decoupling — task in the Signature, strategy in the Module, words generated by DSPy — is the whole point.

Compilation — let an optimiser write the prompt

Here is the part that does not exist in string-template frameworks. You hand DSPy a training set and a metric, and an optimiser (historically called a teleprompter) searches the space of instructions and few-shot demonstrations to maximise your metric. The output is a compiled program with optimised prompts baked in. You measure, you do not guess.

Recommended

Think of the metric as the compiler's objective function. If you cannot write a metric that scores an output as good or bad, you are not ready for DSPy yet — you are ready to build an eval set. Do that first; the optimiser is useless without it.

Manual prompt engineering versus DSPy, side by side

The contrast is sharpest as a table. This is not "DSPy wins everything" — it is "here is what you trade".

Dimension Manual prompt engineering DSPy programmatic optimisation
How prompts are produced Hand-written, edited by intuition and trial-and-error Generated and searched by an optimiser against a metric
Few-shot examples Hand-picked, often stale, provenance unclear Bootstrapped from your data and kept only if they pass the metric
Quality signal Eyeballing a handful of cases A numeric score on a held-out eval set
Swapping the model Re-tune the prompt by hand, hope it transfers Re-compile against the same metric; prompt is regenerated
Reproducibility Lives in one engineer's head and a Git diff Deterministic given data, metric and optimiser config
Upfront cost Low to start; high ongoing tweaking Front-loaded token cost at compile time; amortised after
Learning curve Shallow — anyone can edit a string Steeper — Signatures, Modules, metrics, optimisers

Read the last two rows carefully, because they are the honest cost. DSPy is not free. You pay tokens at compile time while the optimiser runs hundreds of trial generations, and you pay in ramp-up time learning the abstractions. The bet is that those costs are one-off and the savings — no more endless hand-tuning, clean model swaps — recur on every production call thereafter.

The optimisers: which one, and why

The optimiser is where the magic and the money go. DSPy ships several; in practice most teams start with two.

BootstrapFewShot — the cheap first move

BootstrapFewShot runs your program over the training set, collects the runs that pass your metric, and uses those successful traces as few-shot demonstrations in the final prompt. It is bootstrapping in the literal sense — the model teaches itself from its own correct outputs. It is fast, cheap, and the right first thing to try because it answers "do demonstrations help here at all?" before you spend on anything heavier.

MIPROv2 — jointly optimise instructions and examples

MIPROv2 is the heavier hitter. It does not just pick demonstrations; it also proposes natural-language instruction variants and searches the joint space of instructions plus demonstrations using a Bayesian-style search over candidate configurations. When BootstrapFewShot gives you a modest lift and you want more, and you can afford the extra compile-time tokens, this is the next step. It is also the optimiser most likely to surprise you with an instruction phrasing no human would have written.

Optimiser What it searches Compile cost Reach for it when
BootstrapFewShot Few-shot demonstrations bootstrapped from your data Low First attempt; small-to-medium training sets; tight token budget
BootstrapFewShot + random search Demonstrations across several candidate sets Medium Bootstrap helped and you want to squeeze a bit more
MIPROv2 Instructions and demonstrations, jointly High You want the biggest lift and can afford the compile tokens
Pro tip

Start with BootstrapFewShot on a small training set — a few dozen examples is often enough to see signal. Only graduate to MIPROv2 once you have proven the task benefits from optimisation at all. Jumping straight to the most expensive optimiser is the fastest way to burn a token budget and conclude, wrongly, that "DSPy is expensive".

An end-to-end example you can copy

Here is the full loop on the ticket router: configure a model, define a metric, compile with BootstrapFewShot, evaluate, then save the compiled program. This is the shape of nearly every DSPy project.

import dspy
from dspy.teleprompt import BootstrapFewShot
from dspy.evaluate import Evaluate

# 1. Point DSPy at a model (any provider; swapping this is a one-liner)
lm = dspy.LM("openai/gpt-5.2", max_tokens=512)
dspy.configure(lm=lm)

# 2. Define a metric: did we route correctly?
def routing_metric(example, prediction, trace=None) -> bool:
    return (
        prediction.urgency == example.urgency
        and prediction.team == example.team
    )

# 3. A handful of labelled examples is enough to start
trainset = [
    dspy.Example(
        ticket_text="My card was charged twice for the London order.",
        urgency="high", team="billing",
    ).with_inputs("ticket_text"),
    dspy.Example(
        ticket_text="How do I export my Bengaluru team's invoices?",
        urgency="low", team="account",
    ).with_inputs("ticket_text"),
    # ... a few dozen more ...
]

# 4. Compile: the optimiser searches demonstrations against the metric
optimiser = BootstrapFewShot(metric=routing_metric, max_bootstrapped_demos=4)
compiled_router = optimiser.compile(TicketRouter(), trainset=trainset)

# 5. Evaluate the compiled program on a held-out set
evaluate = Evaluate(devset=devset, metric=routing_metric, num_threads=8)
score = evaluate(compiled_router)
print(f"Routing accuracy: {score}")

# 6. Persist the optimised program — load it in production, no re-compile
compiled_router.save("ticket_router.json")
# later, in your service:
#   loaded = TicketRouter()
#   loaded.load("ticket_router.json")

Notice what you never did: you never wrote a prompt. You declared a Signature, chose a strategy Module, wrote a metric that encodes "correct", and let the optimiser produce the words. When GPT-5.2 is superseded, you change line 5 — dspy.LM("...") — re-run compile, and you get a freshly optimised prompt for the new model without touching the logic. That single property is, for many teams, worth the whole framework.

From a verified Builder

"We run a three-stage extraction pipeline for compliance documents across our Bengaluru and London desks. We used to keep two prompt files because GPT and our self-hosted model needed different phrasing. With DSPy we keep one program and one metric, and re-compile per model. The compile bill is real — a few pounds of tokens each run — but it replaced two weeks of quarterly prompt-fiddling, so the trade was obvious."

— Arjun, Verified Builder · Bengaluru, IN

Every article here is written by a Verified Builder. Want your name on the next one?

AI Tech Connect lists AI engineers, founders and researchers across India and the UK — and the people hiring browse it to find them. Adding your profile is free.

Become a Verified Builder →

The metric is the whole game

If there is one thing to internalise, it is this: the optimiser is only ever as good as the metric you give it. A sloppy metric optimises towards sloppy output, confidently and at scale. So the metric is not a detail you bolt on at the end — it is the spine of the project, and it is exactly where your evaluation work pays back.

For deterministic fields — an enum, an ID, a boolean — an exact-match metric like the one above is perfect. For fuzzy outputs — a summary, a rewritten answer, a generated explanation — you need a judge. That is where an LLM-as-a-judge becomes your metric: the judge scores each candidate output, and DSPy optimises against the judge's score. Build the judge and the golden set first, with the discipline in our LLM evaluation suite guide, and DSPy slots straight in on top. If your pipeline is retrieval-augmented, the same logic applies with retrieval-aware metrics — our companion piece on RAG evaluation with faithfulness and context precision covers exactly the metrics you would optimise a DSPy RAG module against.

Watch out

An optimiser will ruthlessly exploit a weak metric. If your metric rewards length, it will pad. If it rewards keyword overlap, it will keyword-stuff. Treat the metric like a reward function in RL — because that is effectively what it is — and pressure-test it on adversarial cases before you let an optimiser loose on it. A metric that is easy to game produces a prompt that games it.

What it costs, and where the cost lands

The single biggest misconception is that DSPy is expensive at inference. It is not. The optimised prompt runs like any other prompt in production. The cost is front-loaded at compile time: while the optimiser searches, it makes many trial generations — bootstrapping demonstrations, scoring candidate instructions, running the metric over and over. MIPROv2 in particular can spend a meaningful number of tokens before it converges.

The right way to think about it is amortisation. You pay the compile cost once (per model, per significant data change), and you spread it over every production call the compiled program then serves. For a pipeline handling thousands of requests a day in Mumbai or Manchester, a one-off compile that costs a few pounds or a few hundred rupees of tokens is a rounding error against the engineering weeks of manual tuning it removes. For a feature that serves ten requests a week, it may never amortise — which is exactly the case where you should not use DSPy.

The other costs are non-token. The learning curve is steeper than editing a string; your team needs to internalise Signatures, Modules, metrics and optimisers before they are productive. And you genuinely cannot start without an eval set and a metric — so if you do not have those, the "cost" of DSPy includes building them first. That is good hygiene regardless, which is why the framework pairs so naturally with the evaluation discipline, but it is real work you cannot skip.

When to reach for DSPy, and when to leave it

Adoption in 2026 says this is still early. In a survey of deployed agents reported in recent work on measuring agents in production, roughly 34% used manual hard-coded prompts, around 45% used manual drafting augmented by LLMs, and only about 9% used prompt optimisers such as DSPy. So if you adopt it, you are ahead of the curve — but you are not on the bleeding edge alone: teams at Cursor, Mistral and Databricks use DSPy in production for systematic agent and RAG pipeline engineering. The serious-team signal is there; the long tail has not caught up.

Reach for it when

  • You have a multi-stage LLM pipeline. The more stages, the worse hand-tuning scales, and the more a compiler earns its keep.
  • You keep re-tuning prompts by hand. If "tweak the prompt" is a recurring ticket, you have a compilation problem wearing a prompt-engineering costume.
  • You want to swap models without re-writing prompts. Re-compiling against a fixed metric is the cleanest model-migration story there is.
  • You already have an eval set and a metric. Then the prerequisite is met and the payoff is immediate.

Skip it when

  • It is a single simple prompt with no eval set. The learning curve and compile cost are not worth it; just write the prompt.
  • You have no metric and no appetite to build one. Without a metric the optimiser has nothing to optimise.
  • The feature is low-volume. The front-loaded compile cost may never amortise.
Pro tip

DSPy and constrained decoding solve different halves of the same problem and stack beautifully. DSPy optimises the content of the prompt for quality; constrained decoding guarantees the structure of the output. Use a DSPy module to produce good values, and our constrained-decoding guide to guarantee they come back in a valid schema. And if your pipeline accepts untrusted input, run the prompt-injection defence playbook over it — an optimised prompt is not a secure one.

A note on workflow and team habits

Adopting DSPy is as much a habit change as a code change. The teams that get value from it stop treating the prompt as a hand-edited artefact and start treating it as a build output — something produced from inputs (data, metric, model, optimiser config) by a deterministic process. That mindset is the same one behind spec-driven development with AI coding agents: write the specification and the checks, let the machine generate the implementation, and review the result against the checks rather than authoring it by hand. DSPy is that pattern applied to prompts. The Signature is the spec; the metric is the check; the optimiser is the generator.

Practically, that means versioning your compiled programs, not your prompt strings; storing the training set and metric alongside the code so a compile is reproducible; and re-compiling on a schedule or on model change rather than reactively patching. For a team split across Bengaluru and London, this is also a collaboration win: the metric is a shared, reviewable artefact in a way that one engineer's lovingly hand-tuned 600-word prompt never is.

Conclusion: prompts are an output, not an artefact

The progression mirrors the one every maturing engineering practice goes through. Stop hand-crafting the thing that should be generated. Declare what you want — typed Signatures, composed Modules — write down how you will measure good, and let an optimiser compile the prompt against your data. BootstrapFewShot to find signal cheaply; MIPROv2 when you want to push further. Measure the lift on your own eval set, because the headline 10–40% figures are someone else's benchmark, not your guarantee.

The dependable payoff is not a magic accuracy number — it is that prompting becomes a reproducible, measurable, model-portable part of your system instead of an artisanal craft that breaks on every upgrade. Adoption is still early in 2026, which means there is an edge in adopting it well now: the teams treating prompts as compiled outputs ship LLM pipelines that improve when the data improves and survive when the model changes. The teams still hand-tuning strings are doing it again next quarter.

If you have not built the eval set and metric DSPy depends on, that is the place to start — read our evaluation suite guide, then come back and let the compiler do the prompting for you.