Configuration: Binding Data with @configured¶
This guide explains how to bind configuration data to your Python classes (typically dataclasses) using the unified @configured decorator, as defined in ADR-0010. A single decorator supports both flat (key–value) and tree (nested) configuration structures.
1. Unified Configuration Model¶
Define your configuration shape as a dataclass and use @configured to tell pico-ioc how to populate it from various sources.
- Decorator:
@configured(prefix: str = "", mapping: Literal["auto", "flat", "tree"] = "auto") - prefix — namespace for configuration keys (e.g.
"MYAPP_"for flat,"app"for tree). - mapping — determines how configuration keys map to dataclass fields (
"auto","flat","tree"). - Sources: Combine multiple configuration providers (environment, files, dicts) via the
configuration(...)builder. - Initialization: Pass the
ContextConfigreturned byconfiguration(...)intoinit(config=...).
Notes: - Auto detection: "auto" behaves as "flat" for simple dataclasses (only primitive fields) and as "tree" when nested or complex fields are present. - The prefix applies to the root of the config domain: - Flat: key prefix, typically uppercase with underscores. - Tree: top-level object name in structured sources (e.g., app: in YAML).
2. Binding Modes (mapping parameter)¶
mapping="flat" (or "auto" for simple dataclasses)¶
Use for flat key–value environments (e.g., os.environ).
- Lookup: Keys like
PREFIX_FIELDNAME(usually uppercase). - Auto detection:
"auto"acts as"flat"if all fields are primitive (str, int, float, bool). - Coercion: Strings are automatically cast to the target field type.
Example (flat mapping):
export MYAPP_SERVICE_HOST="api.example.com"
export MYAPP_SERVICE_PORT="8080"
export MYAPP_DEBUG_MODE="true"
import os
from dataclasses import dataclass
from pico_ioc import configured, configuration, EnvSource, init
@configured(prefix="MYAPP_", mapping="auto")
@dataclass
class ServiceSettings:
service_host: str
service_port: int
debug_mode: bool
timeout: int = 30
os.environ.update({
"MYAPP_SERVICE_HOST": "api.example.com",
"MYAPP_SERVICE_PORT": "8080",
"MYAPP_DEBUG_MODE": "true"
})
ctx = configuration(EnvSource(prefix=""))
container = init(modules=[__name__], config=ctx)
settings = container.get(ServiceSettings)
print(settings.service_host) # api.example.com
print(settings.service_port) # 8080
print(settings.debug_mode) # True
print(settings.timeout) # 30
Field naming conventions: - Field service_port maps to MYAPP_SERVICE_PORT. - Case-insensitive value parsing for booleans ("true", "False", etc.). - Missing keys fall back to dataclass defaults (if provided).
mapping="tree" (or "auto" for nested dataclasses)¶
Use for hierarchical sources like YAML/JSON or structured environment variables.
- Lookup: Nested under the given prefix (e.g.,
app:in YAML). - Auto detection:
"auto"acts as"tree"if the dataclass contains nested dataclasses, lists, dicts, or unions. - Features: Supports nested dataclasses, lists, dicts, discriminated unions, and interpolation.
Example (tree mapping with YAML):
app:
service_name: "My Awesome Service"
database:
driver: "postgresql"
host: "db.example.com"
port: 5432
credentials:
username: "admin"
password: "${ENV:DB_PASS}"
features:
- name: "FeatureA"
enabled: true
- name: "FeatureB"
enabled: false
import os
from dataclasses import dataclass, field
from typing import List
from pico_ioc import configured, configuration, YamlTreeSource, init
os.environ["DB_PASS"] = "secret123"
@dataclass
class DbCredentials:
username: str
password: str
@dataclass
class DatabaseConfig:
driver: str
host: str
port: int
credentials: DbCredentials
@dataclass
class FeatureFlag:
name: str
enabled: bool
@configured(prefix="app", mapping="auto")
@dataclass
class AppConfig:
service_name: str
database: DatabaseConfig
features: List[FeatureFlag] = field(default_factory=list)
ctx = configuration(YamlTreeSource("config.yml"))
container = init(modules=[__name__], config=ctx)
cfg = container.get(AppConfig)
print(cfg.service_name) # My Awesome Service
print(cfg.database.credentials.password) # secret123
Structured environment variables (tree mapping via env): - Many setups support double-underscore separators to express nesting: - APP__DATABASE__HOST="db.example.com" - APP__DATABASE__PORT="5432" - APP__FEATURES__0__NAME="FeatureA" - Use @configured(prefix="APP", mapping="tree") with EnvSource.
3. Combining Sources and Precedence¶
Use the configuration(...) builder to compose multiple sources. Typical patterns: - Base config from a file (YAML/JSON). - Environment variables to override file values. - In-memory dicts for tests or explicit overrides.
Example:
from pico_ioc import configuration, YamlTreeSource, EnvSource, DictSource, init
overrides = {"app": {"service_name": "Override Name"}}
ctx = configuration(
YamlTreeSource("config.yml"),
EnvSource(prefix=""), # overrides file values
DictSource(overrides) # highest precedence overrides
)
container = init(modules=[__name__], config=ctx)
Precedence: - When multiple sources define the same setting, later sources in the configuration(...) call override earlier ones. - Field-level Value annotations (see below) have the highest precedence and bypass sources entirely.
4. Advanced Binding with Annotated¶
Python’s typing.Annotated enables metadata-based extensions to field behavior.
Discriminator for Union types¶
Discriminator chooses which subtype to instantiate based on a field value.
from dataclasses import dataclass
from typing import Annotated, Union
from pico_ioc import configured, configuration, DictSource, Discriminator, init
@dataclass
class Postgres:
kind: str
host: str
port: int
@dataclass
class Sqlite:
kind: str
path: str
@configured(prefix="DB", mapping="tree")
@dataclass
class DbCfg:
model: Annotated[Union[Postgres, Sqlite], Discriminator("kind")]
config_data = {"DB": {"model": {"kind": "Postgres", "host": "localhost", "port": 5432}}}
ctx = configuration(DictSource(config_data))
container = init(modules=[__name__], config=ctx)
db = container.get(DbCfg)
print(db)
Notes: - The discriminator field ("kind") must be present in the input and in each union subtype. - Values must match the subtype’s identifier (e.g., "Postgres" vs "Sqlite").
Value for field-level overrides¶
Value provides the highest precedence override — a field annotated with Value(...) always uses that constant, ignoring environment variables, files, or in-memory overrides.
from dataclasses import dataclass
from typing import Annotated
from pico_ioc import configured, Value, configuration, EnvSource, init
@configured(prefix="SVC_", mapping="auto")
@dataclass
class ApiConfig:
url: str
timeout_seconds: Annotated[int, Value(60)]
retries: int = 3
os.environ.update({
"SVC_URL": "https://api.internal",
"SVC_TIMEOUT_SECONDS": "10", # ignored
"SVC_RETRIES": "5"
})
ctx = configuration(EnvSource(prefix=""))
container = init(modules=[__name__], config=ctx)
api_cfg = container.get(ApiConfig)
print(api_cfg.url) # https://api.internal
print(api_cfg.timeout_seconds) # 60 (from Value)
print(api_cfg.retries) # 5 (from ENV)
5. Defaults, Optional fields, and Validation¶
- Defaults: Dataclass defaults are used when a value is missing in all sources.
- Optional: Use
Optional[T](orT | None) for nullable fields; missing keys becomeNoneunless defaulted. - Post-init validation: Add
__post_init__to perform custom checks and raise errors early.
Example:
from dataclasses import dataclass
from typing import Optional
from pico_ioc import configured
@configured(prefix="APP_", mapping="flat")
@dataclass
class HttpConfig:
host: str
port: int = 80
base_path: Optional[str] = None
def __post_init__(self):
if not (1 <= self.port <= 65535):
raise ValueError("port must be between 1 and 65535")
6. Error Handling and Type Coercion¶
- Missing required keys: If a required field (without default) is missing, binding fails with a descriptive error.
- Type coercion:
- Numbers:
"8080"→int(8080) - Booleans:
"true"/"false","1"/"0"→bool - Lists/dicts (tree sources): parsed according to the structured format (YAML/JSON).
- Union and discriminator mismatches produce errors indicating the offending path and expected variants.
7. Tips and Conventions¶
- Keep prefixes consistent:
- Flat:
MYAPP_for env. - Tree:
appfor file-based configs. - Prefer
"auto"for most cases; switch to explicit"flat"or"tree"when needed. - For environment-only tree configs, use double-underscore separators to express nested structure.
- Use
DictSourcefor tests to avoid filesystem and environment dependencies.
This guide unifies configuration patterns supported by @configured and configuration(...) according to ADR-0010, covering flat and tree mappings, multiple-source composition and precedence, discriminated unions, and inline constant overrides using Annotated[..., Value(...)].