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:
- write a prompt
- send it to a model
- read the answer
- 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.
The Short Explanation
Section titled “The Short Explanation”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 below, 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 textoutput: urgency as one of low, medium, or highThis 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.
What DSPy Changes
Section titled “What DSPy Changes”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 application | DSPy application |
|---|---|
| Developer writes the complete prompt | Developer declares the task and output contract |
| Parsing is custom code | Adapters convert responses into typed fields |
| Prompt wording and application logic are mixed | Task, execution strategy, and control flow are separate |
| Changing models may require prompt rewrites | The same program can be evaluated and re-optimized for another model |
| Improvements are often based on a few manual trials | Improvements 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.
The Core Mental Model
Section titled “The Core Mental Model”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 concept | Plain-language meaning | Familiar analogy |
|---|---|---|
| Signature | A declaration of the task’s inputs and outputs | A form showing required fields |
| Module | The method used to complete the task | A worker following a particular procedure |
| Adapter | The formatter and translator between DSPy and the model | An interpreter translating both directions |
| Prediction | The result returned by a module | A completed form |
| Tool | A function the model can choose to call | A calculator, search box, or database lookup |
| Metric | A rule for scoring a result | A grading rubric |
| Optimizer | An algorithm that tests and improves a program | An 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.
Your First DSPy Program
Section titled “Your First DSPy Program”The following example uses Python, but you do not need to know Python classes yet.
1. Install DSPy
Section titled “1. Install DSPy”python -m pip install -U dspy2. Connect a model
Section titled “2. Connect a model”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.
3. Declare and run a task
Section titled “3. Declare and run a task”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: strmeans the input namedfeedbackis text->separates inputs from outputssummary: strmeans the program must return text namedsummarydspy.Predict(...)means make one direct model prediction using that Signatureresult.summaryaccesses 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.
Signatures: The Task Contract
Section titled “Signatures: The Task Contract”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
Field names carry meaning
Section titled “Field names carry meaning”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 constrain the result
Section titled “Types constrain the result”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 Literalimport dspy
class ClassifyUrgency(dspy.Signature): message: str = dspy.InputField() urgency: Literal["low", "medium", "high"] = dspy.OutputField()
classify = dspy.Predict(ClassifyUrgency)Useful output types include:
strfor textboolfor yes/no decisionsintandfloatfor numberslist[str]for a list of stringsLiteral[...]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.
Shorthand Signatures
Section titled “Shorthand Signatures”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.
Class-based Signatures
Section titled “Class-based Signatures”A class-based Signature gives the task a documented, reusable definition:
from typing import Literalimport 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:
| Code | Purpose |
|---|---|
class TriageSupportMessage(dspy.Signature) | Creates a reusable task definition |
| The triple-quoted text | Supplies the main task instruction |
InputField | Declares information supplied to the task |
OutputField | Declares 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.
Modules: The Execution Strategy
Section titled “Modules: The Execution Strategy”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.
Common built-in Modules
Section titled “Common built-in Modules”| Module | What it does | Appropriate use |
|---|---|---|
Predict | Makes a direct prediction | Extraction, classification, and simple generation |
ChainOfThought | Adds an intermediate reasoning-oriented step | Tasks that benefit from decomposition before producing outputs |
ReAct | Runs a loop that can choose and call tools | Grounded research and action-oriented agents |
ProgramOfThought | Generates and executes code to solve the task | Calculations and problems where executable logic helps |
CodeAct | Uses code as the action space | Code-driven tool interaction |
BestOfN | Generates several attempts and selects one | Tasks where sampling multiple candidates improves quality |
Refine | Iteratively improves a candidate | Outputs that can be checked and revised |
Parallel | Processes several independent items concurrently | Batch workloads |
RLM | Lets a model explore large contexts through code | Large 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.
Reasoning output requires care
Section titled “Reasoning output requires care”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.
Composing A Program With Normal Logic
Section titled “Composing A Program With Normal Logic”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:
__init__declares the submodules the program contains.forwarddefines 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.
Tools And ReAct
Section titled “Tools And ReAct”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.
Adapters: How Declarations Become Prompts
Section titled “Adapters: How Declarations Become Prompts”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:
| Adapter | Main behavior | When it helps |
|---|---|---|
ChatAdapter | Uses DSPy’s field-marker chat format | Default, model-independent starting point |
JSONAdapter | Uses JSON and native structured output where supported | Models that reliably follow schemas |
XMLAdapter | Represents fields with XML structure | Models or integrations that perform better with XML |
TwoStepAdapter | Separates generation from structured extraction | Reasoning 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.
Multimodal Inputs
Section titled “Multimodal Inputs”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.
A Reusable Document Pipeline Pattern
Section titled “A Reusable Document Pipeline Pattern”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.
Using Different Models In One Program
Section titled “Using Different Models In One Program”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.
Inspection, Caching, And Observability
Section titled “Inspection, Caching, And Observability”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.
What DSPy Does Not Solve
Section titled “What DSPy Does Not Solve”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.
When DSPy Is A Good Fit
Section titled “When DSPy Is A Good Fit”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
Current Version Context
Section titled “Current Version Context”The workshop code was prepared with DSPy 3.0.4. As of July 19, 2026:
3.2.1is the latest stable GitHub release3.3.0b1is a beta release- the beta adds experimental
ReActV2and 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.
Continue Learning
Section titled “Continue Learning”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.