Define the Contract
Define the Contract
In chapter 2 we stopped treating the prompt string as the program. We defined a small DSPy module around a signature:
sentence
editorial_goal
local_context
โ
rewritten_text
rationale
confidence
That is better than a handwritten prompt, but it raises the next problem. The program boundary is only as good as the contract we put at that boundary.
This chapter is about signatures from first principles. A signature is not a prompt template. It is a task contract.
1. A weak contract hides too much
DSPy supports compact string signatures:
Compare that with:
class ImproveSentence(dspy.Signature):
"""Improve one sentence while preserving meaning, entities, and voice."""
sentence: str = dspy.InputField(desc="The exact sentence to improve")
editorial_goal: str = dspy.InputField(desc="The reason this edit is wanted")
local_context: str = dspy.InputField(desc="Nearby prose for continuity")
constraints: list[str] = dspy.InputField(
desc="Hard constraints the rewrite must obey"
)
rewritten_text: str = dspy.OutputField(
desc="One replacement sentence, not commentary"
)
rationale: str = dspy.OutputField(desc="Brief explanation of the change")
risk: str = dspy.OutputField(desc="low, medium, or high")
Now the signature says what kind of work is being performed. It gives the caller fields that correspond to real application concepts.
It also makes semantic failure easier to name. If rewritten_text contains a paragraph, the result violates the declared task intent. But rewritten_text is still typed as str, so the type alone does not guarantee one-sentence output. If that boundary must be enforced mechanically, the surrounding application still needs validation.
2. Field names are semantic design
Field names are not neutral. They tell the model and the programmer what the value means.
| Weak field | Stronger field | Why it helps |
|---|---|---|
text |
sentence |
Narrows the unit of editing |
instruction |
editorial_goal |
Describes user intent without prescribing method |
context |
local_context |
Signals bounded nearby prose, not global memory |
answer |
rewritten_text |
Gives downstream code a stable field |
notes |
rationale |
Identifies an explanation, not arbitrary metadata |
score |
risk or confidence |
Separates different judgments |
Writer’s contextual preference judge shows the same discipline. Its signature has context_json, original, candidate, deterministic_signals_json, and ranker_signal_json as inputs, then asks for decision, confidence, reason_codes_json, primary_reason, and explanation. The names reflect the surrounding system: deterministic signals and ranker evidence are not the same thing, and the LM decision is not silently treated as human judgment.
The field names create a small ontology for the task.
3. What belongs in the contract?
For inputs, include the semantic information the LM needs to perform the task. For outputs, expose the information downstream software needs to consume or validate. Keep operational metadata outside the signature unless it actually changes the task the model must perform.
For sentence improvement, these belong:
sentence
editorial_goal
local_context
constraints
These usually do not:
database row id
HTTP request id
current retry count
UI tab name
optimizer run id
Those may be important to the application, but they are not semantic task inputs. Keep them in ordinary Python metadata unless the model needs them.
The same distinction appears in CoCoder’s optimization design. Program inputs exclude evaluation-only fields such as validation results, EngineeringDelta outcomes, and human dispositions. Those fields may exist in the evaluation record, but they must not become task inputs when they contain the outcome being measured. If they cross that boundary, the experiment is contaminated. That is an input-contract failure as well as a data-splitting failure.
Ask this when adding a field:
Would a competent human need this value to perform the task?
Would downstream code need this output to act safely?
Would including this value leak evaluation information?
If the answer is no, the field probably belongs outside the signature.
4. Typed outputs catch silent failures
Current DSPy signatures use Python type annotations to describe structured inputs and outputs. The current documentation presents fields such as Literal[...], Optional[...], lists, and structured values as part of the task interface. DSPy’s adapters use that declared structure when formatting and parsing LM interactions, but the exact failure behavior depends on the adapter and provider.
For example:
from typing import Literal
import dspy
class ImproveSentence(dspy.Signature):
"""Improve one sentence while preserving meaning, entities, and voice."""
sentence: str = dspy.InputField()
editorial_goal: str = dspy.InputField()
local_context: str = dspy.InputField()
constraints: list[str] = dspy.InputField()
rewritten_text: str = dspy.OutputField()
rationale: str = dspy.OutputField()
risk: Literal["low", "medium", "high"] = dspy.OutputField()
confidence: float = dspy.OutputField(desc="Confidence from 0.0 to 1.0")
Types do not make the model honest, and they are not a substitute for application validation. They make the expected interface more explicit and give the DSPy parsing layer more structure to work with.
If the model effectively produces "pretty safe" where the contract expects Literal["low", "medium", "high"], that mismatch can surface during parsing or validation instead of being treated as an intentionally valid risk category. The exact behavior is adapter-dependent, so downstream code should still validate any field that controls persistence, ranking, or policy.
Typed fields are especially useful when:
- downstream code branches on the output;
- invalid values would be silently accepted;
- the field has a small known vocabulary;
- the field should be a list, number, boolean, or structured object.
They are less useful when the output is naturally open-ended prose.
5. Strong contracts can be too strong
Over-specification is the opposite failure.
class OverSpecifiedImproveSentence(dspy.Signature):
"""Rewrite the sentence in twelve to sixteen words using active voice,
one comma at most, no semicolons, no adverbs, and a stronger final verb."""
sentence: str = dspy.InputField()
rewritten_text: str = dspy.OutputField()
This might be appropriate for a copyediting constraint test. It is probably wrong as the general sentence-improvement contract. It bakes one editorial theory into the task boundary.
There are three common forms of over-specification:
| Failure | Example | Consequence |
|---|---|---|
| Implementation-history leakage | “Use the ranker-approved style” | Encodes a previous system’s judgment as task truth |
| Method leakage | “Think step by step and compare alternatives” | Prevents swapping execution strategy cleanly |
| Policy leakage | “Always accept if confidence > 0.8” | Mixes model output with application decision policy |
The contract should make evaluation and composition easier. If adding a field makes downstream behavior less clear, reconsider it.
6. A runnable contract check
Even without calling a model, we can test the ordinary Python boundary around the signature. The example below validates the application payload we intend to send toward the LM-facing program and the payload we require back. It does not validate a dspy.Prediction object directly; it represents the application boundary around that prediction.
from typing import Literal
from pydantic import BaseModel, Field
class RewriteInput(BaseModel):
sentence: str = Field(min_length=1)
editorial_goal: str = Field(min_length=1)
local_context: str = ""
constraints: list[str] = Field(default_factory=list)
class RewriteOutput(BaseModel):
rewritten_text: str = Field(min_length=1)
rationale: str = ""
risk: Literal["low", "medium", "high"]
confidence: float = Field(ge=0.0, le=1.0)
def validate_prediction(raw: dict) -> RewriteOutput:
return RewriteOutput.model_validate(raw)
DSPy handles the LM-facing program. Pydantic handles the application-facing validation. In a real system you often want both, because an LM contract and a persistence/API contract are related but not identical.
What Usually Goes Wrong
| Symptom | Likely cause | How to diagnose it | What to change |
|---|---|---|---|
rewritten_text contains multiple sentences or commentary |
Output scope is only semantically declared | Inspect sentence count, newlines, and extra prose | Keep the field description narrow and enforce one-sentence shape at the application boundary |
| The model follows stale product policy | Policy was embedded in the signature | Search signature docstrings for deployment rules | Move policy to deterministic application code |
| Optimization later appears to improve by cheating | Evaluation labels leaked into inputs | Audit fields passed into examples | Remove validation/human-decision fields from program inputs |
| Downstream code has many fallback branches | Output contract is too loose | Count invalid/missing output fields | Add typed fields or narrower output names |
Conclusion
We gained a better contract for the running program. The signature now names what the LM consumes and produces, and those names reflect application semantics rather than prompt-writing convenience.
We removed the assumption that “the model will understand what I mean” is enough of an interface for software.
What remains unsolved is execution. We have defined what the program should do, but not how the LM should attempt it.
That leads to the next question:
Why should the task specification be independent from the reasoning strategy used to execute it?