Skip to content

Object Instantiation

ReausoConfig's key feature is object-oriented configuration: your config files describe object relationships, and the library instantiates a fully-connected object graph by calling actual class constructors.

This enables full object instantiation - not just data containers, but factories, service objects, logic components, and complete application wiring. Your entire application can be assembled from configuration.

How It Works

When you call rc.instantiate():

  1. Config loading: The config file is loaded and composed (resolving _ref_ references)
  2. Validation: The config structure is validated against registered class constructors
  3. Recursive instantiation: Each nested config with a _target_ becomes an actual object instance
  4. Constructor mapping: Config keys become constructor keyword arguments
@dataclass
class Database:
    host: str
    port: int

@dataclass
class Service:
    name: str
    db: Database  # Nested object

rc.register(name="database", target=Database)
rc.register(name="service", target=Service)
# service.yaml
_target_: service
name: "api"
db:
  _target_: database
  host: "localhost"
  port: 5432
# This creates: Service(name="api", db=Database(host="localhost", port=5432))
service = rc.instantiate(path=Path("service.yaml"))

# Result is pure Python objects - no framework dependency
assert isinstance(service, Service)
assert isinstance(service.db, Database)
assert service.db.host == "localhost"

Beyond Data Classes: Factories and Logic Objects

ReausoConfig works with any callable - not just dataclasses. Constructor parameters don't need to be stored as attributes. This makes it perfect for factories, builders, and objects that perform logic during initialization:

class Rectangle:
    """Constructor values are used for computation, not stored directly."""
    def __init__(self, width: float, height: float):
        self._area = width * height
        self._perimeter = 2 * (width + height)

    @property
    def area(self) -> float:
        return self._area

class ConnectionPool:
    """Factory that creates internal resources from config values."""
    def __init__(self, host: str, port: int, pool_size: int):
        self._connections = [
            self._create_connection(host, port)
            for _ in range(pool_size)
        ]

    def _create_connection(self, host: str, port: int):
        # Create actual connection...
        pass

class ApplicationBootstrapper:
    """Orchestrates application startup from config."""
    def __init__(self, db: ConnectionPool, cache: ConnectionPool, workers: int):
        self._db = db
        self._cache = cache
        self._start_workers(workers)

    def _start_workers(self, count: int):
        # Initialize worker threads...
        pass

rc.register(name="rectangle", target=Rectangle)
rc.register(name="pool", target=ConnectionPool)
rc.register(name="app", target=ApplicationBootstrapper)
# app.yaml - Wire your entire application from config
_target_: app
workers: 4
db:
  _target_: pool
  host: "db.example.com"
  port: 5432
  pool_size: 10
cache:
  _target_: pool
  host: "cache.example.com"
  port: 6379
  pool_size: 5
# One call bootstraps your entire application
app = rc.instantiate(path=Path("app.yaml"))

Type-Driven Polymorphism

Type hints enable runtime substitution of implementations:

from abc import ABC, abstractmethod

class Optimizer(ABC):
    @abstractmethod
    def step(self): pass

class Adam(Optimizer):
    def __init__(self, lr: float):
        self.lr = lr
    def step(self): pass

class SGD(Optimizer):
    def __init__(self, lr: float, momentum: float):
        self.lr = lr
        self.momentum = momentum
    def step(self): pass

@dataclass
class Trainer:
    optimizer: Optimizer  # Accepts any Optimizer subclass

rc.register(name="adam", target=Adam)
rc.register(name="sgd", target=SGD)
rc.register(name="trainer", target=Trainer)
# Switch implementations by changing _target_ - no code changes needed
_target_: trainer
optimizer:
  _target_: adam  # or "sgd"
  lr: 0.001

Framework Independence

After instantiation, your objects have no dependency on ReausoConfig:

  • Objects are pure Python instances of your classes
  • No base classes or mixins required
  • No framework imports needed in your application code
  • Works with dataclasses, regular classes, or any callable

This means your application code remains clean and testable - only your startup/configuration code needs to import rconfig.

Nested Configs

Configs can contain nested configs that are instantiated recursively:

_target_: trainer
model:
  _target_: model
  hidden_size: 256
optimizer:
  _target_: optimizer
  lr: 0.001

Target Resolution

ReausoConfig can automatically determine the target class for nested configs in two ways:

1. Implicit Inference from Type Hints

When a nested config field has a concrete type hint (a class that is registered with no subclasses), the _target_ can be omitted and will be automatically inferred:

@dataclass
class ModelConfig:
    hidden_size: int

@dataclass
class TrainerConfig:
    model: ModelConfig  # Concrete type - _target_ can be inferred
    epochs: int

rc.register(name="model", target=ModelConfig)
rc.register(name="trainer", target=TrainerConfig)
# _target_ for model is optional here
_target_: trainer
model:
  hidden_size: 256  # No _target_ needed - inferred from type hint
epochs: 10

2. Auto-registration from Explicit Targets

When a nested config has an explicit _target_ that matches the expected type's class name (case-insensitive), the class is automatically registered if not already:

@dataclass
class ResNet:
    layers: int
    pretrained: bool

@dataclass
class TrainerConfig:
    model: ResNet  # Type hint provides the class
    epochs: int

rc.register(name="trainer", target=TrainerConfig)
# Note: ResNet is NOT registered manually
_target_: trainer
model:
  _target_: resnet  # Auto-registers ResNet (matches class name)
  layers: 50
  pretrained: false
epochs: 100

This is useful with _ref_ composition - referenced files can specify their own _target_ without pre-registration:

# models/resnet.yaml (referenced via _ref_)
_target_: resnet
layers: 50
pretrained: false

When _target_ is Required

  • Root config file - Always required
  • Abstract base classes - Cannot be instantiated directly
  • Base classes with multiple registered subclasses - Ambiguous which to use
  • Union types - Cannot determine which type to use
from abc import ABC, abstractmethod

class BaseEncoder(ABC):
    @abstractmethod
    def encode(self): pass

class TransformerEncoder(BaseEncoder):
    def __init__(self, layers: int):
        self.layers = layers
    def encode(self): pass

class LSTMEncoder(BaseEncoder):
    def __init__(self, hidden_size: int):
        self.hidden_size = hidden_size
    def encode(self): pass

@dataclass
class Model:
    encoder: BaseEncoder  # Abstract - _target_ required!

rc.register(name="transformer", target=TransformerEncoder)
rc.register(name="lstm", target=LSTMEncoder)
rc.register(name="model", target=Model)
_target_: model
encoder:
  _target_: transformer  # Required - BaseEncoder is abstract
  layers: 6

Auto-registration Requirements

  • Parent field must have a type hint
  • _target_ name must match the type hint's class name (case-insensitive)
  • The class must not be abstract
  • The type hint must be a single class (not Union[A, B])

When auto-registration fails:

  • TargetNotFoundError: Target name doesn't match expected class
  • AmbiguousTargetError: Type is abstract or has multiple implementations