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():
- Config loading: The config file is loaded and composed (resolving
_ref_references) - Validation: The config structure is validated against registered class constructors
- Recursive instantiation: Each nested config with a
_target_becomes an actual object instance - 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)
# 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
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 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:
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)
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 classAmbiguousTargetError: Type is abstract or has multiple implementations