Quickstart#

Let’s consider a data loader class that reads a CSV file from disk.

import pandas as pd

class DataLoader:

    def __init__(self, df_path: str):
        self.df_path = df_path

    def load(self):
        return pd.read_csv(self.df_path)

if __name__ == '__main__':
    loader = DataLoader('path/to/data')
    data = loader.load()

What if we want to run multiple DataLoader instances, each pointing to a different df_path?

The issue is that df_path is mixed into the code logic itself. Changing it means touching the class or its instantiation site — both of which tend to spread as a project grows.

A cleaner approach is to separate code logic from configuration:

class DataLoaderConfig:

    def __init__(self, df_path: str):
        self.df_path = df_path

class DataLoader:

    def __init__(self, config: DataLoaderConfig):
        self.config = config

    def load(self):
        return pd.read_csv(self.config.df_path)

if __name__ == '__main__':
    config = DataLoaderConfig(df_path='path/to/data')
    loader = DataLoader(config)
    data = loader.load()

We now rely on dependency injection — the DataLoader does not know or care where its parameters come from.

Note

The DataLoader’s API does not change as we swap configurations.

Cinnamon#

Cinnamon formalises this pattern and adds validation, registration, and dependency resolution on top of it.

Define your configuration by subclassing Configuration and declaring each field as a typed class annotation, optionally wrapped with Param:

from pathlib import Path
from cinnamon.configuration import Configuration, Param

class DataLoaderConfig(Configuration):
    df_path: Path = Param(
        'path/to/data',
        description='Path to the CSV file to load'
    )

Define your component as an ordinary class. There is no base class to inherit and nothing to import — cinnamon imposes no APIs on your code logic:

class DataLoader:

    def __init__(self, df_path: Path):
        self.df_path = df_path

    def load(self):
        return pd.read_csv(self.df_path)

To use both together without the registry:

if __name__ == '__main__':
    config = DataLoaderConfig.default()
    loader = DataLoader(**config.values)
    data = loader.load()

config.values returns a plain {field_name: value} dictionary, which unpacks directly into the component’s constructor.

Note

DataLoaderConfig.default() is equivalent to DataLoaderConfig(). It returns a DataLoaderConfig instance with all fields set to their defaults.

Registration#

In practice, cinnamon encourages a register, bind, and build workflow rather than directly instantiating configurations and components.

        flowchart LR
    A["register<br/>store the configuration under a key"]
    B["bind<br/>name the component by import path"]
    C["build<br/>Registry.build() resolves everything"]
    D["from_key(key)<br/>a ready component"]
    A --> B --> C --> D
    

Once you have defined a Configuration and its corresponding component, you register the configuration in the Registry and bind it to the component. This is done via a RegistrationKey: a compound identifier made up of a name, an optional tags set, and a namespace.

The most concise way to register is the @register_class decorator on the Configuration itself:

from cinnamon.configuration import Configuration, Param
from cinnamon.registry import register_class

@register_class(
    name='data_loader',
    tags={'test'},
    namespace='showcasing',
    component='components.DataLoader'   # module path as a string
)
class DataLoaderConfig(Configuration):
    df_path: Path = Param(
        'path/to/data',
        description='Path to the CSV file to load'
    )

When one Configuration class has to register under several keys, decorate the @classmethod that builds each of them with @register_method instead.

Alternatively, you can register programmatically using Registry.register_configuration() inside a function decorated with @register:

from cinnamon.registry import Registry, register

@register
def register_data_loader():
    Registry.register_configuration(
        config=DataLoaderConfig.default(),
        name='data_loader',
        tags={'test'},
        namespace='showcasing',
        component='components.DataLoader'
    )

Both approaches are equivalent. The @register_method style is more concise when the registration lives naturally on the configuration class. The @register style is useful when you want to re-use an existing Configuration without subclassing it.

Building#

The Registry does not execute registrations eagerly. Instead, call Registry.build() to scan your project’s configurations folder, run all @register and @register_method decorators it finds, and resolve the full dependency graph:

from pathlib import Path
from cinnamon.registry import Registry

Registry.build(directory=Path('.'))

After a successful build, construct a DataLoader instance from its registered key:

from cinnamon.registry import RegistrationKey

# From a key
key = RegistrationKey(name='data_loader', tags={'test'}, namespace='showcasing')
loader = Registry.from_key(key)

# Or with the key spelled out, optionally checking the type of the result
loader = Registry.instantiate(
    name='data_loader',
    tags={'test'},
    namespace='showcasing',
    expected_type=DataLoader,
)

data = loader.load()

The Registry builds the DataLoaderConfig instance, resolves any dependencies, validates all conditions, and passes config.values to DataLoader.__init__ automatically.

To swap the underlying implementation, you only need to change the RegistrationKey — the calling code stays the same.

Beyond quickstart#

The register, bind, and build workflow unlocks a number of powerful features:

  • Nesting components and configurations to compose more sophisticated pipelines.

  • Automatically generating Configuration variants for hyperparameter search.

  • Integrating external components and configurations written by other users.

  • Static and dynamic condition validation.

The tutorial is the natural next step: seven runnable files that introduce each of these one at a time, ending with a worked project laid out the way a real one is.

For reference depth, Concepts covers each idea in turn: parameters, conditions and variants; how to structure registration code and the Registry APIs; and how keys, containers and variants propagate through the dependency graph. Cinnamon entry points covers the four cmn-* commands.

Before you run anything#

cmn-check resolves the whole registry and reports what is wrong: unresolved keys, with a suggestion for what you probably meant, and components whose __init__ does not match the configuration bound to them.

cmn-check -dir .

It imports none of your components to do it, so it stays fast on a project that takes a minute to import, and it exits non-zero when it finds errors — which makes it usable as a commit hook or a CI step.