Configuration
A Configuration is a Pydantic BaseModel subclass
that stores all the parameters of a Component.
Each field is defined as a typed class annotation, optionally wrapped with Param to attach
cinnamon-specific metadata such as descriptions, tags, and variants.
Param
Param is a wrapper around Pydantic’s Field that accepts all standard Field arguments
and additionally supports three cinnamon-specific keyword arguments:
tags: asetof arbitrary string keywords for grouping and searching parameters.variants: alistof alternative values for automated configuration search.description: a human-readable description of the parameter.
from cinnamon.configuration import Configuration, Param
class MyConfig(Configuration):
x: int = Param(5, description='An example integer parameter', tags={'number'})
The first positional argument is the default value.
Passing ... (Ellipsis) marks the field as required — no default will be used and
Pydantic will raise a ValidationError if the field is omitted at instantiation.
class MyConfig(Configuration):
x: int = Param(...) # required: must be provided at instantiation
y: int = Param(5) # optional: defaults to 5
Plain Python defaults are also accepted without Param.
In that case, cinnamon automatically injects empty tags and variants metadata:
class MyConfig(Configuration):
x: int = 5 # equivalent to Param(5, tags=set(), variants=[])
Accessing parameter metadata
Each Configuration exposes a meta descriptor that provides access to field metadata
(tags and variants) at both class level and instance level.
class MyConfig(Configuration):
x: int = Param(5, tags={'number'}, variants=[10, 20])
# Class-level access (reads from model_fields)
MyConfig.meta.x.tags # >>> {'number'}
MyConfig.meta.x.variants # >>> [10, 20]
# Instance-level access (reads from a per-instance copy of metadata)
config = MyConfig()
config.meta.x.tags # >>> {'number'}
config.meta.x.variants # >>> [10, 20]
# Bracket access is also supported
config.meta['x'].tags # >>> {'number'}
Instance-level metadata is independent across instances — modifying one instance’s metadata does not affect other instances or the class definition.
config_a = MyConfig()
config_b = MyConfig()
config_a.meta.x.variants.append(30)
config_a.meta.x.variants # >>> [10, 20, 30]
config_b.meta.x.variants # >>> [10, 20] (unchanged)
Type constraints
Since Param forwards all keyword arguments to Pydantic’s Field, standard Pydantic
field constraints apply directly.
For numeric ranges, use ge (≥), le (≤), gt (>), lt (<):
class MyConfig(Configuration):
x: int = Param(5, ge=1, le=10) # 1 <= x <= 10
MyConfig(x=5) # ✓
MyConfig(x=0) # ✗ ValidationError
For discrete allowed values, use Literal:
from typing import Literal
class MyConfig(Configuration):
mode: Literal['train', 'eval', 'test'] = Param('train')
MyConfig(mode='train') # ✓
MyConfig(mode='other') # ✗ ValidationError
For cross-field constraints, use @model_validator:
from pydantic import model_validator
class MyConfig(Configuration):
x: int = Param(10)
y: int = Param(5)
@model_validator(mode='after')
def check_x_greater_than_y(self) -> 'MyConfig':
if self.x <= self.y:
raise ValueError(
f'x must be greater than y, got x={self.x}, y={self.y}'
)
return self
MyConfig(x=10, y=5) # ✓
MyConfig(x=3, y=5) # ✗ ValidationError
Configuration
A Configuration is defined by subclassing Configuration and declaring fields
as typed class annotations:
class MyConfig(Configuration):
x: int = Param(50, description='An example parameter')
config = MyConfig()
print(config.x) # >>> 50
Field values are accessed as regular Python attributes.
Since Configuration validates defaults at instantiation (validate_default=True),
a misconfigured default is caught immediately:
class InvalidConfig(Configuration):
x: int = Param(5, ge=1, le=3)
InvalidConfig() # ✗ ValidationError: x=5 violates le=3
To create a modified copy of an existing instance, use model_copy:
config = MyConfig()
updated = config.model_copy(update={'x': 10})
print(updated.x) # >>> 10
print(config.x) # >>> 50 (original unchanged)
model_copy re-validates the updated fields, so constraints are enforced:
class MyConfig(Configuration):
x: int = Param(5, ge=1, le=10)
config = MyConfig()
config.model_copy(update={'x': 99}) # ✗ ValidationError
Accessing field values and definitions
All field values are accessible as a plain dictionary via the values property:
class MyConfig(Configuration):
x: int = Param(5)
y: bool = Param(True)
config = MyConfig()
config.values # >>> {'x': 5, 'y': True}
All FieldInfo definitions (type, default, constraints, metadata) are accessible
via the fields property, which mirrors model_fields:
config.fields # >>> {'x': FieldInfo(...), 'y': FieldInfo(...)}
config.fields['x'] # >>> FieldInfo(default=5, ...)
Adding conditions
Beyond Pydantic’s built-in field validation, Configuration supports runtime
conditions: arbitrary callables that check invariants across one or more fields.
class MyConfig(Configuration):
x: int = Param(10)
y: int = Param(5)
config = MyConfig()
config.add_condition(
name='x_equals_y',
condition=lambda c: c.x == c.y,
description='x and y must be equal'
)
The condition name must be unique. Registering a second condition with the same name
raises a RuntimeWarning.
Conditions accept an optional tags set for grouping:
config.add_condition(
name='x_positive',
condition=lambda c: c.x > 0,
tags={'sanity'},
description='x must be positive'
)
Note
Conditions registered via add_condition are evaluated lazily — they are
not checked at instantiation time. Use @model_validator for constraints
that should be enforced at construction.
Validating conditions
Conditions registered via add_condition are evaluated explicitly by calling
validate_conditions:
class MyConfig(Configuration):
x: int = Param(10)
y: int = Param(5)
config = MyConfig()
config.add_condition(name='match', condition=lambda c: c.x == c.y)
config.validate_conditions() # ✗ raises ValidationFailureException
config.validate_conditions(strict=False) # returns ValidationResult(passed=False, ...)
config = config.model_copy(update={'x': 5})
config.add_condition(name='match', condition=lambda c: c.x == c.y)
config.validate_conditions() # ✓ passes silently
When strict=False, a ValidationResult is returned instead of raising an exception,
allowing callers to inspect the result programmatically:
result = config.validate_conditions(strict=False)
result.passed # >>> False
result.error_message # >>> 'Condition match failed!'
If a Configuration has nested Configuration dependencies, validate_conditions
recursively validates them as well.
Searching fields by tag
Tags allow quickly retrieving fields that share a keyword:
class MyConfig(Configuration):
x: int = Param(10, tags={'number'})
y: bool = Param(True, tags={'boolean'})
z: int = Param(30, tags={'number'})
config = MyConfig()
number_fields = {
name: config.meta[name]
for name in config.fields
if 'number' in config.meta[name].tags
}
# >>> {'x': ParamMeta(...), 'z': ParamMeta(...)}
To filter by multiple tags, extend the condition:
target_tags = {'number', 'hyperparameter'}
matching = {
name: config.meta[name]
for name in config.fields
if target_tags & config.meta[name].tags # non-empty intersection
}
Default template
Configuration subclasses define their fields at class level — no explicit default()
override is needed in the typical case.
The inherited default() classmethod is equivalent to calling the constructor with no
arguments:
class MyConfig(Configuration):
x: int = Param(5)
config = MyConfig.default()
# equivalent to: config = MyConfig()
config.x # >>> 5
For configurations that require custom initialisation logic, default() can be overridden:
class MyConfig(Configuration):
x: int = Param(5)
@classmethod
def default(cls) -> 'MyConfig':
config = super().default()
config.add_condition(
name='x_positive',
condition=lambda c: c.x > 0
)
return config
Inheritance works naturally — a subclass inherits all fields from its parent and may add new ones:
class MyConfig(Configuration):
x: int = Param(5)
class MyConfigExtension(MyConfig):
y: bool = Param(True)
config = MyConfigExtension.default()
config.x # >>> 5 (inherited)
config.y # >>> True
Variants
Instead of defining multiple Configuration subclasses for slight parameter variations,
cinnamon supports variants: alternative values declared alongside a field’s default.
class CustomConfig(Configuration):
x: int = Param(5, variants=[20, 42])
y: bool = Param(False, variants=[True])
The default value must not appear in variants — cinnamon enforces this at
instantiation and raises a ValidationError if a duplicate is detected.
The variants property returns all unique combinations of variant values, excluding
the all-default combination (which is the configuration itself):
config = CustomConfig.default()
combos = config.variants
Each entry in the returned list is a dict with two keys:
values: the field values for this combination.indexes: the index of each value within its field’s variant list (0= default).
# combos[0] = {'values': {'x': 5, 'y': True}, 'indexes': {'x': 0, 'y': 1}}
# combos[1] = {'values': {'x': 20, 'y': False}, 'indexes': {'x': 1, 'y': 0}}
# combos[2] = {'values': {'x': 20, 'y': True}, 'indexes': {'x': 1, 'y': 1}}
# combos[3] = {'values': {'x': 42, 'y': False}, 'indexes': {'x': 2, 'y': 0}}
# combos[4] = {'values': {'x': 42, 'y': True}, 'indexes': {'x': 2, 'y': 1}}
Use model_copy to instantiate a specific variant:
variant = config.model_copy(update=combos[0]['values'])
variant.x # >>> 5
variant.y # >>> True
Nesting (i.e., adding dependencies)
Configuration instances can be nested to compose more sophisticated configurations.
Nesting is expressed via RegistrationKey values — loose pointers resolved at
build time by the Registry.
from cinnamon.registry import RegistrationKey
class ChildConfig(Configuration):
z: int = Param(42)
class ParentConfig(Configuration):
param_1: bool = Param(True, variants=[False, True])
param_2: bool = Param(False, variants=[False, True])
child: RegistrationKey = Param(
RegistrationKey(name='child', tags={'default'}, namespace='testing')
)
The dependencies property returns all fields whose value is a RegistrationKey
or a nested Configuration instance:
config = ParentConfig.default()
config.dependencies
# >>> {'child': RegistrationKey(name='child', tags={'default'}, namespace='testing')}
Note
In cinnamon, nested configurations are called dependencies.
See dependencies
for how the Registry resolves and builds them automatically.