prerequisite
Python essentials for DSPy
Just enough Python to read and run every sample in this cluster: classes and inheritance, type hints, keyword arguments, callables, and virtual environments.
Before this
Nothing beyond first-year college math. This is a starting page.
Why you need this
DSPy is a Python library, and its samples lean on a handful of features that a first programming course often rushes: classes, subclasses that override a method, objects you can call like functions, type hints, keyword arguments, and functions passed around as values. A DSPy program is a class with a forward method, a signature is a class whose type hints DSPy reads, and a metric is a function you hand to an optimizer. This page shows each feature in plain Python with no DSPy code. Every snippet ran on Python 3.14; anything from 3.12 on behaves the same.
The idea
Classes and __init__
A class is a template for making objects. An object (or instance) is one thing made from it, carrying its own data in attributes. A method is a function defined inside the class; Python passes the object itself as the first argument, named self by convention. The special method __init__ runs once when the object is created and sets its starting attributes.
# excerpt of shapes.py, shown in full under Worked example
class Step:
"""Base class: calling a step runs its forward method."""
def __init__(self, name: str):
self.name = name
self.calls = 0
Step("shout") creates an object whose name is "shout" and whose calls is 0.
Subclasses that override a method
A subclass is a class built on another class, its parent. Write the parent in parentheses: class Shout(Step). The subclass gets every method of the parent for free and can override one by defining a method with the same name. When a subclass needs its own __init__, it calls super().__init__(...) first so the parent's setup still happens.
# excerpt of shapes.py
class Repeat(Step):
def __init__(self, name: str, times: int = 2):
super().__init__(name)
self.times = times
def forward(self, text: str) -> str:
return " ".join([text] * self.times)
This is exactly the shape of a DSPy module: you subclass dspy.Module, call super().__init__(), create the parts you need in __init__, and override forward to say what happens when the module runs.
Objects you can call: __call__
If a class defines __call__, its objects can be called like functions. step(text="go") runs step.__call__(text="go"). In the example, the parent's __call__ counts the call and then runs forward. The subclasses never touch __call__; they only provide forward. DSPy does the same thing: you call a module, and the library's __call__ does its own bookkeeping (recording history and usage) before running your forward. That is why DSPy samples call program(question=...) and never program.forward(question=...).
Type hints, and why DSPy reads them
A type hint is a note on a name saying what type it should hold: text: str means "a string", -> str after the parentheses means "returns a string", times: int = 2 is an integer with a default of 2. Python does not enforce hints when the code runs. They are stored on the function, and any program can read them:
print(Shout.forward.__annotations__)
prints {'text': <class 'str'>, 'return': <class 'str'>}. DSPy reads hints this way. In a class-based signature, sentiment: bool tells DSPy to ask the model for a true or false value and to convert the reply into a Python bool. Common hint forms you will see: int, float, bool, str, list[str] (a list of strings), and Literal["positive", "negative"] (exactly one of these strings, imported from typing).
Keyword arguments and **kwargs
A keyword argument is passed by name: Repeat("repeat", times=3). DSPy modules insist on keyword arguments for their inputs, because the names must match the signature's field names. A function written with **kwargs collects any keyword arguments it was not expecting into a dictionary named kwargs. In a call, **settings does the reverse: it unpacks a dictionary into keyword arguments. You will see **kwargs throughout DSPy, for example when a module passes model settings through to the model.
Functions as values
In Python a function is a value like any other. You can store it in a variable or pass it to another function without calling it (no parentheses). A metric in DSPy is exactly this: a function that scores one output, handed to an evaluator or an optimizer that calls it on every example.
# excerpt of shapes.py
def exact_match(expected: str, got: str) -> bool:
return expected == got
def run_checks(step, cases, metric):
passed = 0
for text, expected in cases:
if metric(expected, step(text=text)):
passed += 1
return passed / len(cases)
run_checks(..., metric=exact_match) passes the function itself; run_checks decides when to call it.
Virtual environments and pip install
A virtual environment is a private folder of installed packages for one project, so two projects can use different library versions without clashing. pip is Python's package installer. The usual setup, run once per project from its folder:
python -m venv .venv
.venv\Scripts\activate (Windows)
source .venv/bin/activate (macOS and Linux)
pip install dspy
After activating, python and pip refer to the copies inside .venv. The samples in this cluster ran in such an environment with DSPy 3.4 installed.
Worked example
A tiny class hierarchy with the same shape as a DSPy program: a base class whose __call__ runs forward, two subclasses that override forward, and a Pipeline that holds two steps as attributes and wires them together in its own forward.
# shapes.py
class Step:
"""Base class: calling a step runs its forward method."""
def __init__(self, name: str):
self.name = name
self.calls = 0
def __call__(self, **kwargs):
self.calls += 1
return self.forward(**kwargs)
def forward(self, **kwargs):
raise NotImplementedError("subclasses must define forward")
class Shout(Step):
def forward(self, text: str) -> str:
return text.upper() + "!"
class Repeat(Step):
def __init__(self, name: str, times: int = 2):
super().__init__(name)
self.times = times
def forward(self, text: str) -> str:
return " ".join([text] * self.times)
class Pipeline(Step):
def __init__(self):
super().__init__("pipeline")
self.first = Repeat("repeat", times=3)
self.second = Shout("shout")
def forward(self, text: str) -> str:
middle = self.first(text=text)
return self.second(text=middle)
p = Pipeline()
print(p(text="go"))
print(p.calls, p.first.calls, p.second.calls)
print(Shout.forward.__annotations__)
try:
Step("bare")(text="hi")
except NotImplementedError as e:
print("error:", e)
def exact_match(expected: str, got: str) -> bool:
return expected == got
def run_checks(step, cases, metric):
passed = 0
for text, expected in cases:
if metric(expected, step(text=text)):
passed += 1
return passed / len(cases)
cases = [("go", "GO!"), ("hi", "HI!"), ("ok", "ok")]
print(run_checks(Shout("s"), cases, metric=exact_match))
def describe(**kwargs):
for key, value in kwargs.items():
print(f"{key} = {value!r}")
settings = {"temperature": 0.0, "max_tokens": 1000}
describe(model="local", **settings)
python shapes.py prints:
GO GO GO!
1 1 1
{'text': <class 'str'>, 'return': <class 'str'>}
error: subclasses must define forward
0.6666666666666666
model = 'local'
temperature = 0.0
max_tokens = 1000
Trace the first line by hand:
| Step | Call | Result |
|---|---|---|
| 1 | p(text="go") runs Step.__call__, which runs Pipeline.forward |
|
| 2 | self.first(text="go") runs Repeat.forward with times = 3 |
"go go go" |
| 3 | self.second(text="go go go") runs Shout.forward |
"GO GO GO!" |
Each of the three objects was called once, so each calls counter is 1. The base class alone has no real forward, so calling it raises the error. The checker scores 2 of 3 cases because Shout turns "ok" into "OK!", not "ok": . The last call unpacks the settings dictionary into two keyword arguments next to model.
In an optimization pipeline
Stage 1, Define the program, is classes: a signature is a class with type hints, a module is a subclass of dspy.Module with forward, and a bigger program holds smaller modules as attributes, just like Pipeline. Stage 2, Measure it, is functions as values: you write a metric and pass it in. Stage 3, Optimize the prompts, works because the program is an object: the optimizer walks its attributes to find each step and edits their instructions and examples. See Signatures and modules and Configuring language models for the DSPy versions of these shapes.
Common mistakes
- Forgetting
super().__init__()in a subclass. Attributes the parent sets never exist, and a later line fails withAttributeError. - Calling
forwarddirectly instead of the object. The output looks right, but the bookkeeping in__call__is skipped (in DSPy, history and usage tracking). - Passing inputs by position.
step("go")fails with aTypeErrorhere; DSPy modules raise an error that names the keyword arguments they expect. - Writing
metric=exact_match()with parentheses. That calls the function immediately with no arguments, and Python stops with aTypeErrorabout missing arguments. - Trusting type hints to check values. Python runs the code anyway; a wrong type fails later, somewhere less obvious.
- Installing into the system Python.
import dspyworks in one terminal and fails in another, or two projects fight over versions.
Cost
None of this costs model calls. The cost is your time: a missing super().__init__() or a misnamed keyword argument is a few minutes of reading a traceback. Virtual environments cost disk space, one copy of each library per project.
Going further
- The Python tutorial's chapter on classes, for inheritance and special methods.
- The
typingmodule documentation, forLiteral,Optional, and generic types likelist[str]. - The
venvmodule documentation, and the tooluvas a faster installer. - Signatures and modules, where these shapes become DSPy code.
Leads to
- techniqueConfiguring language modelsConnecting DSPy to a local task model served by Ollama and to Claude Opus 5.5 for reflection, with settings, contexts, caching, and usage tracking.
- techniqueSignatures and modulesWriting a task as typed inputs and outputs instead of a prompt string, and running it through Predict, ChainOfThought, and other built-in modules.
Back to DSPy and GEPA: programming and optimizing language-model systems