Signatures: Declaring What, Not How

A DSPy signature is the contract that says what goes in and what comes out of a step — and by declaring the transformation instead of writing the prompt, it becomes something the optimizer can improve.

The first primitive of DSPy is the signature, and it is where the framework’s whole philosophy is concentrated. A signature declares the what of a transformation — its inputs, its outputs, and their meaning — without specifying the how of the prompt. This second post in the DSPy series covers signatures in both their forms, why typed fields matter, and how the signature becomes the thing DSPy compiles.

What a signature is

A signature is a specification of a language-model transformation: what fields it takes and what fields it produces. It is deliberately not a prompt. Where a prompt says “You are an expert assistant. Read the following question carefully and provide a concise, accurate answer…”, a signature says question -> answer. The signature captures the intent; DSPy is responsible for turning it into an actual prompt, and the optimizer is responsible for making that prompt good.

That separation is the point. Because the signature declares intent rather than wording, the wording is free to be generated, tuned, and re-tuned. You commit to what the step does and leave how to ask to the framework.

Inline signatures

The quickest form is a string. An inline signature names input and output fields separated by ->:

import dspy
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))

summarize = dspy.Predict("document -> summary")
classify  = dspy.Predict("ticket -> urgency")
translate = dspy.Predict("english_text -> french_text")

Multiple fields on either side are comma-separated, and the field names are meaningful — DSPy uses them to construct the prompt, so question, context -> answer reads and prompts better than input1, input2 -> output:

answer = dspy.Predict("context, question -> answer")

Inline signatures are perfect for quick work and prototyping. The field names carry the semantics, so choose them to describe the data honestly.

Class-based signatures

When you need more control — descriptions, types, or a task-level instruction — use a class-based signature. It is a dspy.Signature subclass whose docstring is the task description and whose fields are declared with InputField and OutputField:

class ClassifyUrgency(dspy.Signature):
    """Classify a support ticket's urgency for triage."""

    ticket: str = dspy.InputField(desc="the customer's message")
    urgency: str = dspy.OutputField(desc="one of: low, medium, high, critical")

classify = dspy.Predict(ClassifyUrgency)

Several things are happening here, all of which help the model and the optimizer:

Class-based signatures are what you reach for in real systems, because the descriptions and constraints materially improve results and give the optimizer more to work with.

Typed fields and structure

Signatures are not limited to strings. Output fields can be typed — booleans, numbers, lists, or structured types — and DSPy uses the type to enforce and parse the output. Declaring is_spam: bool or scores: list[float] tells DSPy to produce and validate that shape, which is far more reliable than asking for JSON in a prompt and hoping. This connects DSPy to the constrained-output discipline: the signature’s types are a validation boundary, not a suggestion. When your downstream code needs structured data, encode that in the signature’s output types and let DSPy handle the extraction.

The signature is what gets compiled

Here is why the signature matters beyond convenience. When you later attach an optimizer, what it tunes is anchored to the signature: it proposes better instructions (rewrites of that docstring) and selects few-shot demonstrations that fit the signature’s fields. The signature is the stable contract — inputs, outputs, intent — around which the optimizer searches for the best prompt. A vague signature gives the optimizer little to anchor on; a precise one, with honest field names and clear descriptions, gives it a strong starting point and a clear target. Writing good signatures is therefore not busywork — it is the highest-leverage authoring you do in DSPy, because everything downstream compiles against them.

Signatures compose

Because each signature is a self-contained contract, signatures compose cleanly into pipelines. A RAG program might use question -> search_query to rewrite the query, then context, question -> answer to generate — two signatures, each declaring one transformation, wired together in a program (the subject of a later post). Each can be understood, tested, and optimized on its own terms. This composability is what lets DSPy scale from a single call to a multi-step system without the pipeline collapsing into one giant unmaintainable prompt.

Key takeaways

Further reading

Sources & References

Signatures