Subclassing
Building Programs by Subclassing
In Lesson 1a, you learned to build programs using the Functional API. Now, let's explore a more advanced approach: subclassing the Program class.
When to Use Subclassing
Subclassing is useful when you need:
- Custom logic in your program's execution flow
- Stateful behavior that persists across calls
- Reusable components that can be shared across projects
- Full control over serialization and deserialization
Think of it like the difference between using a pre-built function vs writing your own class in object-oriented programming.
The Subclassing Pattern
When you subclass synalinks.Program, you must implement:
__init__(): Define your modules and initialize statecall(): Define how data flows through your modulesget_config(): Define how to save your program (serialization)from_config(): Define how to load your program (deserialization)
classDiagram
class Program {
+__init__()
+call(inputs)
+get_config()
+from_config(config)
}
class MyProgram {
+generator
+__init__(language_model)
+call(inputs)
+get_config()
+from_config(config)
}
Program <|-- MyProgram
class MyProgram(synalinks.Program):
def __init__(self, language_model=None):
super().__init__() # Always call super().__init__()!
self.generator = synalinks.Generator(
data_model=OutputModel,
language_model=language_model,
)
async def call(self, inputs, training=False):
# Define the forward pass
return await self.generator(inputs)
def get_config(self):
# Return a dict with everything needed to recreate this program
return {"language_model": serialize(self.language_model)}
@classmethod
def from_config(cls, config):
# Recreate the program from the config dict
return cls(language_model=deserialize(config["language_model"]))
Important: The build() Method
Unlike the Functional API, subclassed programs need to be built before first use. This tells Synalinks what input type to expect:
program = MyProgram(language_model=lm)
await program.build(InputDataModel) # <-- Required before first call!
If you used a subclassed module inside a functional API program, your module is built automatically!
Complete Example
import asyncio
from dotenv import load_dotenv
import synalinks
class Query(synalinks.DataModel):
query: str = synalinks.Field(description="The user query")
class AnswerWithThinking(synalinks.DataModel):
thinking: str = synalinks.Field(description="Your step by step thinking")
answer: str = synalinks.Field(description="The correct answer")
class ChainOfThought(synalinks.Program):
def __init__(self, language_model=None):
super().__init__()
self.answer_generator = synalinks.Generator(
data_model=AnswerWithThinking,
language_model=language_model,
)
async def call(self, inputs, training=False):
return await self.answer_generator(inputs)
def get_config(self):
return {
"language_model": synalinks.saving.serialize_synalinks_object(
self.language_model
)
}
@classmethod
def from_config(cls, config):
language_model = synalinks.saving.deserialize_synalinks_object(
config.pop("language_model")
)
return cls(language_model=language_model)
async def main():
load_dotenv()
language_model = synalinks.LanguageModel(model="openai/gpt-4.1")
program = ChainOfThought(language_model=language_model)
await program.build(Query) # Required before first call!
result = await program(Query(query="What is 15% of 80?"))
print(f"Answer: {result['answer']}")
asyncio.run(main())
Key Takeaways
- Subclassing: Inherit from
synalinks.Programfor full control over program behavior and custom logic. - Four Methods: Implement
__init__(),call(),get_config(), andfrom_config()for a complete subclassed program. - Build Required: Call
await program.build(InputDataModel)before first use when using standalone subclassed programs. - Serialization:
get_config()andfrom_config()enable saving and loading your custom programs.
Program Visualization
API References
AnswerWithThinking
Bases: DataModel
The output from our program - reasoning + final answer.
Source code in examples/1b_subclassing.py
ChainOfThought
Bases: Program
A program that answers questions with step-by-step reasoning.
Note: The first line of the docstring becomes the program's description if not explicitly provided in super().init().
Source code in examples/1b_subclassing.py
call(inputs, training=False)
async
Define how data flows through your program.
This method is called when you do await program(input_data).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inputs
|
JsonDataModel
|
The input data (will be a Query instance) |
required |
training
|
bool
|
Whether we're in training mode (for optimization) |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
JsonDataModel |
JsonDataModel
|
The output data (will be an AnswerWithThinking instance) |
Source code in examples/1b_subclassing.py
from_config(config)
classmethod
Recreate the program from a configuration dict.
This is called when loading the program from disk.
Source code in examples/1b_subclassing.py
get_config()
Return configuration needed to recreate this program.
This is called when saving the program to disk.
