Skip to content

DSPy: Programming Language Models

DSPy is a Python framework for building applications that use language models.

You can understand its main idea even if you do not write software. A normal LLM workflow often looks like this:

  1. write a prompt
  2. send it to a model
  3. read the answer
  4. rewrite the prompt when the answer is not good enough

That works for experimentation. It becomes difficult when an application has several model calls, uses different models, calls tools, needs structured outputs, or must be measured across hundreds of examples.

DSPy replaces much of that manual prompt editing with a programming model. You describe:

  • what information goes into a task
  • what information must come out
  • which strategy should perform the task
  • how several tasks connect
  • how success will be measured

DSPy then constructs the model messages, parses the response, and can later optimize parts of the program against examples and a metric.

Suppose you ask an LLM to classify a support message.

Without DSPy, you might write a long prompt:

Read the support message below. Return JSON with an urgency field that must be
low, medium, or high. Do not return Markdown. Make sure the JSON is valid...

You then write additional code to parse the JSON, reject invalid values, retry failures, and pass the result to the next prompt.

With DSPy, you define the task more directly. In plain language, the contract is:

input: message as text
output: urgency as one of low, medium, or high

This says:

  • the input is text called message
  • the output is called urgency
  • only three output values are acceptable

This declaration is called a Signature. DSPy turns it into model instructions through an Adapter, executes it through a Module, and returns a typed Prediction.

DSPy does not remove prompts. Language models still receive text, messages, schemas, examples, images, tool definitions, and other context.

It changes who manages those details.

Prompt-centered applicationDSPy application
Developer writes the complete promptDeveloper declares the task and output contract
Parsing is custom codeAdapters convert responses into typed fields
Prompt wording and application logic are mixedTask, execution strategy, and control flow are separate
Changing models may require prompt rewritesThe same program can be evaluated and re-optimized for another model
Improvements are often based on a few manual trialsImprovements can be measured over a dataset

The phrase “programming, not prompting” describes this shift. It does not mean prompt design has disappeared. Names, descriptions, instructions, examples, and tool documentation still influence the model. DSPy makes them inspectable and optimizable parts of a larger program.

flowchart TD
    A["Signature<br/>What goes in and what must come out"] --> B["Module<br/>How the task is performed"]
    B --> C["Adapter<br/>How fields become model messages"]
    C --> D["Language model"]
    D --> C
    C --> E["Prediction<br/>Typed result returned to the program"]
    B --> F["Python control flow<br/>Rules, loops, routing, and validation"]
    B --> G["Tools<br/>Search, APIs, databases, and calculations"]
DSPy conceptPlain-language meaningFamiliar analogy
SignatureA declaration of the task’s inputs and outputsA form showing required fields
ModuleThe method used to complete the taskA worker following a particular procedure
AdapterThe formatter and translator between DSPy and the modelAn interpreter translating both directions
PredictionThe result returned by a moduleA completed form
ToolA function the model can choose to callA calculator, search box, or database lookup
MetricA rule for scoring a resultA grading rubric
OptimizerAn algorithm that tests and improves a programAn experiment runner trying better instructions

The first five concepts are covered on this page. Metrics and optimizers are covered in Evaluating and Optimizing DSPy Programs.

The following example uses Python, but you do not need to know Python classes yet.

Terminal window
python -m pip install -U dspy
import dspy
lm = dspy.LM("openai/gpt-5.4-mini")
dspy.configure(lm=lm)

dspy.LM(...) describes the model connection. dspy.configure(...) makes that model the default for later DSPy calls.

The provider and model name are replaceable. DSPy supports many model providers; credentials are normally supplied through environment variables or provider configuration, not embedded in source code. Model names change over time, so use one supported by your provider and installed DSPy version.

summarize = dspy.Predict("feedback: str -> summary: str")
result = summarize(
feedback="The export works, but it takes several minutes and gives no progress update."
)
print(result.summary)

Read it in plain language:

  • feedback: str means the input named feedback is text
  • -> separates inputs from outputs
  • summary: str means the program must return text named summary
  • dspy.Predict(...) means make one direct model prediction using that Signature
  • result.summary accesses the named output instead of parsing free-form text

DSPy builds the model messages and returns an object similar to:

Prediction(summary="The export is slow and does not show progress.")

The Signature describes what the task is. Predict describes how to attempt it.

A Signature declares the behavior expected from one model-facing step.

It can include:

  • one or more inputs
  • one or more outputs
  • Python types
  • instructions
  • descriptions for individual fields

The language model reads field names. They are not merely internal labels.

dspy.Predict("message -> urgency")

communicates more intent than:

dspy.Predict("x -> y")

Clear names such as customer_message, contract_clause, source_passages, and risk_level give the model useful context before a longer instruction is added.

Types make the expected result more precise. Fixed-choice and other rich Python types are best expressed with a class-based Signature:

from typing import Literal
import dspy
class ClassifyUrgency(dspy.Signature):
message: str = dspy.InputField()
urgency: Literal["low", "medium", "high"] = dspy.OutputField()
classify = dspy.Predict(ClassifyUrgency)

Useful output types include:

  • str for text
  • bool for yes/no decisions
  • int and float for numbers
  • list[str] for a list of strings
  • Literal[...] for a fixed set of allowed labels
  • Pydantic models, dataclasses, and typed dictionaries for nested structures

Types are still not proof that the model’s conclusion is correct. They make the response easier to parse and reject malformed structures, but the content must still be evaluated.

String Signatures are useful for small tasks and experiments:

extract = dspy.Predict(
"email: str -> sender_name: str, requires_reply: bool"
)

They are compact, but complex instructions become hard to read inside one string.

A class-based Signature gives the task a documented, reusable definition:

from typing import Literal
import dspy
class TriageSupportMessage(dspy.Signature):
"""Classify a support message without inventing missing facts."""
message: str = dspy.InputField(
desc="The customer's original support message"
)
urgency: Literal["low", "medium", "high"] = dspy.OutputField(
desc="How quickly a human should review the message"
)
reason: str = dspy.OutputField(
desc="A brief explanation grounded only in the message"
)
triage = dspy.Predict(TriageSupportMessage)
result = triage(message="Production checkout is rejecting every payment.")

What each part does:

CodePurpose
class TriageSupportMessage(dspy.Signature)Creates a reusable task definition
The triple-quoted textSupplies the main task instruction
InputFieldDeclares information supplied to the task
OutputFieldDeclares information the model must return
Literal[...]Restricts urgency to known labels
dspy.Predict(TriageSupportMessage)Chooses direct prediction as the execution strategy

Use a class-based Signature when a task has several fields, richer types, detailed instructions, or is shared by several modules.

A Signature says what result is required. A Module says how to produce it.

The same Signature can be passed to different modules:

direct = dspy.Predict(TriageSupportMessage)
deliberate = dspy.ChainOfThought(TriageSupportMessage)

Both return the declared urgency and reason fields. They use different inference strategies.

ModuleWhat it doesAppropriate use
PredictMakes a direct predictionExtraction, classification, and simple generation
ChainOfThoughtAdds an intermediate reasoning-oriented stepTasks that benefit from decomposition before producing outputs
ReActRuns a loop that can choose and call toolsGrounded research and action-oriented agents
ProgramOfThoughtGenerates and executes code to solve the taskCalculations and problems where executable logic helps
CodeActUses code as the action spaceCode-driven tool interaction
BestOfNGenerates several attempts and selects oneTasks where sampling multiple candidates improves quality
RefineIteratively improves a candidateOutputs that can be checked and revised
ParallelProcesses several independent items concurrentlyBatch workloads
RLMLets a model explore large contexts through codeLarge documents or datasets that should not be placed in one prompt

A more elaborate Module is not automatically better. It may make more model calls, cost more, take longer, or introduce additional failure modes. Start with Predict, measure it, and add a stronger strategy only when the task needs one.

ChainOfThought remains a supported DSPy Module. However, an emitted reasoning field should not be treated as a guaranteed faithful explanation of the model’s internal process. Use declared task outputs and external evidence for decisions, audits, and verification.

DSPy Modules can be combined with ordinary Python rules. This is important because not every decision belongs to an LLM.

class SupportRouter(dspy.Module):
def __init__(self):
super().__init__()
self.triage = dspy.Predict(TriageSupportMessage)
def forward(self, message: str):
assessment = self.triage(message=message)
# Routing is a deterministic business rule.
queue = "on_call" if assessment.urgency == "high" else "support"
return dspy.Prediction(
urgency=assessment.urgency,
reason=assessment.reason,
queue=queue,
)

There are two phases:

  1. __init__ declares the submodules the program contains.
  2. forward defines what happens when the program runs.

The LLM classifies the message. Normal code chooses the queue from the approved rule. This keeps deterministic policy outside the model and makes it easy to test.

Use ordinary code for:

  • authorization and access checks
  • monetary calculations
  • database writes and transactions
  • fixed routing rules
  • validation and safety limits
  • retries, timeouts, and error handling

Use an LLM where interpretation, extraction, semantic classification, or generation is actually required.

A model cannot know current weather, private account data, or the result of a database query from its weights. It needs tools.

In DSPy, a tool can be an ordinary Python function with a clear name, typed arguments, and a useful docstring:

def search_knowledge_base(query: str) -> list[str]:
"""Return relevant approved support articles for a search query."""
return knowledge_base.search(query, limit=3)

Pass the function to ReAct:

answer_support_question = dspy.ReAct(
"question: str -> answer: str",
tools=[search_knowledge_base],
max_iters=4,
)
result = answer_support_question(
question="How do I rotate an expired API key?"
)

ReAct manages an inference-time loop:

flowchart TD
    A["Question"] --> B["Model chooses the next step"]
    B -->|Call a tool| C["Tool executes in application code"]
    C --> D["Observation returned to the model"]
    D --> B
    B -->|Finish| E["Declared answer fields"]
    B -->|Iteration limit| F["Stop or handle failure"]

max_iters prevents an unbounded loop, but it is not a complete safety system. The application must still control:

  • which tools are exposed
  • what identity and permissions each tool uses
  • argument validation
  • network and filesystem access
  • timeouts and rate limits
  • confirmation before consequential actions
  • logging and error handling

Tool names, parameter names, and docstrings influence how the model selects and calls tools. They should be designed as carefully as Signature fields.

DSPy’s stable ReAct API is used here. DSPy 3.3.0b1 also introduced an experimental ReActV2; beta APIs should not be the foundation of production documentation until they stabilize.

An LLM provider does not understand a Python Signature directly. It receives messages or provider-specific structured data. The Adapter performs that translation.

flowchart LR
    A["Signature, values, and examples"] --> B["Adapter formats messages"]
    B --> C["Model request"]
    C --> D["Raw model response"]
    D --> E["Adapter parses and coerces fields"]
    E --> F["Typed Prediction"]

The current official DSPy adapter model includes:

AdapterMain behaviorWhen it helps
ChatAdapterUses DSPy’s field-marker chat formatDefault, model-independent starting point
JSONAdapterUses JSON and native structured output where supportedModels that reliably follow schemas
XMLAdapterRepresents fields with XML structureModels or integrations that perform better with XML
TwoStepAdapterSeparates generation from structured extractionReasoning output that is useful but difficult to parse directly

Set a process-wide adapter:

dspy.configure(
lm=lm,
adapter=dspy.JSONAdapter(),
)

Or override it for one block:

with dspy.context(adapter=dspy.XMLAdapter()):
result = triage(message="The billing page shows the wrong currency.")

The Signature and Module do not change. This separation is useful when moving between models with different formatting strengths.

The workshop demonstrates a BAML-oriented adapter. A BAMLAdapter implementation still exists in the DSPy source tree, but it is not one of the main adapters exported and emphasized by the current public API. Treat it as an advanced integration rather than the default path.

DSPy Signatures are not limited to text. Current DSPy types include Image, Audio, and File.

class ReadParkingSign(dspy.Signature):
"""Read the visible parking rules and state whether parking is allowed now."""
sign: dspy.Image = dspy.InputField()
current_time: str = dspy.InputField()
allowed: bool = dspy.OutputField()
restriction: str = dspy.OutputField()
read_sign = dspy.Predict(ReadParkingSign)
result = read_sign(
sign=dspy.Image("parking-sign.jpg"),
current_time="Tuesday 12:30 PM",
)

The selected language model must support the supplied modality. DSPy can format the field correctly, but it cannot give an image-blind model vision capability.

The workshop repository also uses the separate attachments package to load PDFs and expose extracted text and page images to DSPy. That package is an integration used by the example, not a core DSPy primitive. Current DSPy also has a built-in File type; choose a file-processing approach based on the model provider, document size, OCR requirements, and retrieval strategy.

One durable lesson from the workshop is not a particular PDF demo. It is the architecture: use small typed stages, then route results with ordinary code.

flowchart TD
    A["Incoming file"] --> B["Extract text and page images"]
    B --> C["Classify document type"]
    C --> D{"Route by typed result"}
    D -->|Regulatory form| E["Extract a structured schema"]
    D -->|Contract| F["Summarize and detect sections"]
    D -->|Image| G["Run visual analysis"]
    E --> H["Validate required fields"]
    F --> I["Verify page boundaries"]
    G --> J["Check result against policy"]

This pattern demonstrates several important choices:

  • each model call has a narrow Signature
  • intermediate outputs are typed
  • Python code owns routing and concurrency
  • different stages can use different models
  • deterministic validation follows model interpretation
  • each stage can be evaluated separately

The workshop examples use deliberately distinct document categories and prototype-level checks. Production classification needs representative test data, ambiguous and unknown categories, confidence or abstention handling, and review paths for consequential results.

DSPy lets a program have a default model and temporarily select another model for a specific workload:

fast_lm = dspy.LM("provider/fast-model")
vision_lm = dspy.LM("provider/vision-model")
dspy.configure(lm=fast_lm)
classification = classify_document(document_text=text)
with dspy.context(lm=vision_lm):
visual_result = read_sign(
sign=dspy.Image("parking-sign.jpg"),
current_time="Tuesday 12:30 PM",
)

The identifiers above are placeholders. Replace them with provider and model identifiers supported by your environment. The durable concept is the scoped dspy.context(...) override, not a particular model name.

Model routing should be based on evidence rather than assumptions. Evaluate candidate models on the same representative examples, including cost and latency, before assigning them to stages.

The abstraction does not mean the prompt is hidden permanently.

Inspect recent model interactions:

dspy.inspect_history(n=1)

This helps answer:

  • what messages did the Adapter generate?
  • which examples were inserted?
  • what raw output did the model return?
  • why did a field fail to parse?

DSPy also includes caching, callbacks, usage tracking, and async helpers. External observability integrations can trace calls across a larger application. The workshop uses Arize Phoenix instrumentation as one example.

Caching is helpful during development, but remember what it means: a repeated call may return a stored result instead of contacting the model. Disable or clear the relevant cache when testing model changes, nondeterminism, latency, or provider behavior.

Do not log secrets, personal data, or confidential document contents merely because model history and traces are convenient to inspect.

DSPy provides useful abstractions, but it does not automatically provide:

  • correct answers
  • representative evaluation data
  • secure authorization
  • durable workflow execution
  • business transactions
  • human approval
  • document retention policy
  • production monitoring
  • protection from prompt injection
  • a guarantee that one model can replace another

DSPy can be part of an agent or workflow. It is not a replacement for application architecture, security controls, or operational infrastructure.

Consider DSPy when:

  • an application contains several model-facing steps
  • structured inputs and outputs matter
  • prompt strings are mixed with parsing and business logic
  • you need to compare models systematically
  • the same task runs often enough to justify evaluation
  • you have or can build examples and metrics
  • you want to optimize instructions or demonstrations

It may be unnecessary when:

  • a person is having a one-time conversation with an LLM
  • the task is a tiny prototype with no repeated behavior
  • deterministic code can solve the problem completely
  • the team cannot define what a good result means

The workshop code was prepared with DSPy 3.0.4. As of July 19, 2026:

  • 3.2.1 is the latest stable GitHub release
  • 3.3.0b1 is a beta release
  • the beta adds experimental ReActV2 and a new LM runtime direction

The Signatures, Modules, Adapters, tools, metrics, and optimizer concepts in the workshop remain current. This page uses stable public APIs and treats beta behavior as version-specific.

Next, read Evaluating and Optimizing DSPy Programs to learn how examples and metrics turn a DSPy program into something that can be measured and improved.

Diagram viewer