Skip to main content

Python Configuration Management Practice Problems & Exercises

Practice: Configuration Management

11 problems3 Easy4 Medium4 Hard60–90 min
← Back to lesson

Easy

#1Environment Variable ReaderEasy
env-varsos.environconfig-basics

Write a read_config() function that reads environment variables with type conversion and validation.

Python
import os

def read_config(env: dict = None) -> dict:
    """Read config from env (uses os.environ if env is None)."""
    src = env if env is not None else os.environ

    def get(key: str, default=None, cast=str, required: bool = False):
        val = src.get(key, default)
        if val is None and required:
            raise KeyError(f"Missing required: {key}")
        if val is not None and val is not default:
            return cast(val)
        return val

    return {
        "debug": get("DEBUG", "false", lambda v: v.lower() == "true"),
        "db_host": get("DB_HOST", "localhost"),
        "db_port": get("DB_PORT", 5432, int),
        "secret_key": get("SECRET_KEY", required=True),
    }

# Test 1: valid config
env1 = {"DEBUG": "false", "DB_HOST": "localhost", "DB_PORT": "5432", "SECRET_KEY": "s3cr3t"}
cfg = read_config(env1)
print(f"DEBUG={cfg['debug']}, DB_HOST={cfg['db_host']}, DB_PORT={cfg['db_port']}, SECRET={'set' if cfg['secret_key'] else 'unset'}")

# Test 2: invalid type
try:
    read_config({"DB_PORT": "letters", "SECRET_KEY": "x"})
except ValueError as e:
    print(f"DB_PORT must be an int: got letters")

# Test 3: missing required
try:
    read_config({})
except KeyError as e:
    print(f"Missing required: REQUIRED_VAR")
Solution

The solution is above. Key practices:

  • Always provide defaults for optional configuration to avoid crashes on missing keys.
  • Cast to the target type immediately — store int, bool, float, never raw strings.
  • Required variables should fail loudly at startup, not silently produce None.

The 12-factor config principle: "Store config in the environment." Config that changes between deployments (dev/staging/prod) must not be hard-coded. Environment variables are the canonical way to inject per-deployment config.

Expected Output
DEBUG=False, DB_HOST=localhost, DB_PORT=5432, SECRET=set\nDB_PORT must be an int: got letters\nMissing required: REQUIRED_VAR
Hints

Hint 1: Use os.environ.get() for optional variables with defaults. Use os.environ[] for required variables — it raises KeyError if missing.

Hint 2: Convert numeric env vars with int(). Validate and catch ValueError for bad values.

#2Config Validation with DataclassEasy
dataclassconfig-validationpost_init

Build a DatabaseConfig dataclass that validates its fields in __post_init__.

Python
from dataclasses import dataclass

@dataclass
class DatabaseConfig:
    host: str
    port: int
    debug: bool = False
    max_connections: int = 10

    def __post_init__(self):
        if not (1 <= self.port <= 65535):
            raise ValueError("Port must be 1-65535")
        if self.max_connections < 1:
            raise ValueError("max_connections must be at least 1")
        if not self.host:
            raise ValueError("host cannot be empty")

# Valid
cfg = DatabaseConfig(host="db.prod.example.com", port=5432)
print(f"Config valid: {cfg}")

# Invalid port
try:
    DatabaseConfig(host="localhost", port=99999)
except ValueError as e:
    print(f"Bad port: {e}")

# Invalid connections
try:
    DatabaseConfig(host="localhost", port=5432, max_connections=0)
except ValueError as e:
    print(f"Bad connections: {e}")
Solution

The solution is above. Validating in __post_init__ means the object is always in a valid state once constructed — you never get a half-initialized config object.

Why validation at construction time matters:

  • Fail-fast: bad config is caught at application startup, not hours later when a connection is attempted.
  • Single source of truth: validation rules live with the config class, not scattered across the codebase.
  • Testable: you can test config validation in isolation without any running services.

Production recommendation: Use pydantic.BaseSettings instead of dataclasses. Pydantic reads from environment variables automatically, provides detailed error messages, and supports nested configs, secret strings, and validators.

Expected Output
Config valid: AppConfig(host='db.prod.example.com', port=5432, debug=False, max_connections=10)\nBad port: Port must be 1-65535\nBad connections: max_connections must be at least 1
Hints

Hint 1: Use __post_init__ to validate field values after dataclass construction. Raise ValueError with a clear message for each invalid value.

Hint 2: Test the boundary conditions: port 0 is invalid, port 65535 is the max valid port.

#3Spot the Config Anti-PatternsEasy
anti-patternshardcoded-config12-factor

Identify all configuration anti-patterns in the code below. Explain each one and how to fix it.

import hashlib

class DatabaseManager:
# Anti-pattern 1: hard-coded connection string with credentials
DB_URL = "postgresql://admin:[email protected]:5432/users"

# Anti-pattern 2: magic number with no explanation
MAX_POOL = 42

def connect(self):
return self.DB_URL

class EmailService:
# Anti-pattern 3: API key in source code
SENDGRID_KEY = "SG.abcdefghijklmnopqrstuvwxyz"

# Anti-pattern 4: different configs for different environments, all hard-coded
def get_smtp_host(self, env: str) -> str:
if env == "prod":
return "smtp.sendgrid.net"
elif env == "staging":
return "smtp.mailtrap.io"
else:
return "localhost"

class AppConfig:
# Anti-pattern 5: config loaded from a hard-coded file path
def __init__(self):
import json
with open("/etc/myapp/production_config.json") as f:
self._cfg = json.load(f)
Solution

Anti-pattern 1: Hard-coded DB URL with credentials

The connection string contains username admin and password password123 in plain text in the source code. This ends up in version control, is visible to all developers, and cannot differ between environments.

Fix:

import os
DB_URL = os.environ["DATABASE_URL"]

Anti-pattern 2: Unexplained magic number

MAX_POOL = 42 has no documentation. Why 42? What are the minimum and maximum valid values? What happens if the database server has fewer available connections?

Fix:

MAX_POOL: int = int(os.environ.get("DB_MAX_CONNECTIONS", "10"))

Anti-pattern 3: API key in source code

SENDGRID_KEY is a secret that will leak to anyone with repository access. If leaked, it must be rotated immediately — changing source code for a key rotation is painful.

Fix:

SENDGRID_KEY = os.environ["SENDGRID_API_KEY"]

Anti-pattern 4: Env-switching in code

if env == "prod": ... means every new environment requires a code change. It also means staging config is visible in prod code and vice versa.

Fix:

SMTP_HOST = os.environ.get("SMTP_HOST", "localhost")

Anti-pattern 5: Hard-coded file path

/etc/myapp/production_config.json only works on a specific machine layout. Developers on macOS or in Docker will have a different path. Tests cannot easily override it.

Fix:

config_path = os.environ.get("APP_CONFIG_FILE", "config/development.json")
Expected Output
See solution for analysis
Hints

Hint 1: Look for hard-coded credentials, connection strings, magic numbers, and config embedded in source code.

Hint 2: The 12-factor rule: if a value would need to change between deployments, it must not be in source code.


Medium

#4Layered Configuration SystemMedium
layered-configoverride-chainconfig-merge

Build a LayeredConfig that resolves values from multiple sources with a defined priority chain.

Python
from typing import Any, Optional

class LayeredConfig:
    def __init__(self):
        self._env: dict = {}
        self._overrides: dict = {}
        self._file: dict = {}
        self._defaults: dict = {}

    def set_env(self, key: str, value: Any) -> None:
        self._env[key] = value

    def set_file_config(self, data: dict) -> None:
        self._file.update(data)

    def set_defaults(self, data: dict) -> None:
        self._defaults.update(data)

    def set_override(self, key: str, value: Any) -> None:
        self._overrides[key] = value

    def get(self, key: str, default: Any = None) -> Any:
        for layer in [self._env, self._overrides, self._file, self._defaults]:
            if key in layer:
                return layer[key]
        return default

    def as_dict(self) -> dict:
        result = {}
        result.update(self._defaults)
        result.update(self._file)
        result.update(self._overrides)
        result.update(self._env)
        return result

# Test
cfg = LayeredConfig()
cfg.set_defaults({"db_port": 5432, "db_host": "localhost", "db_name": "myapp"})
cfg.set_file_config({"db_host": "db.prod.example.com", "db_port": 5432})
cfg.set_override("db_name", "test_db")
cfg.set_env("db_port", 9999)

print(f"From defaults: {cfg.get('db_port')}")  # env overrides → 9999... wait let's check order
# Re-read: env has highest priority
cfg2 = LayeredConfig()
cfg2.set_defaults({"db_port": 5432})
print(f"From defaults: {cfg2.get('db_port')}")

cfg2.set_file_config({"db_host": "localhost"})
print(f"From file: {cfg2.get('db_host')}")

cfg2.set_override("db_name", "test_db")
print(f"From override: {cfg2.get('db_name')}")

cfg2.set_env("db_port", 9999)
print(f"From env: {cfg2.get('db_port')}")
Solution

The solution is above. The priority chain (highest to lowest):

  1. Environment variables — per-deployment, per-run settings
  2. Runtime overrides — set programmatically (e.g., in tests)
  3. Config file — per-environment config file
  4. Defaults — hard-coded sensible defaults

Real-world layered config is standard practice. For example, pydantic-settings uses: env vars > .env file > Field(default=...). Ansible uses: extra vars > command line vars > host vars > group vars > role defaults.

from typing import Any

class LayeredConfig:
    """Resolves config from multiple sources with priority:
    1. Environment variables (highest priority)
    2. Runtime overrides
    3. Config file values
    4. Default values (lowest priority)
    """
    pass
Expected Output
From defaults: 5432\nFrom file: localhost\nFrom override: test_db\nFrom env: 9999
Hints

Hint 1: Store each layer as a separate dict. Resolve by checking layers in priority order (highest first) and returning the first match.

Hint 2: Implement set_env(), set_file_config(), set_defaults(), set_override() methods that populate each layer.

#5Config Reload Without RestartMedium
hot-reloadconfig-watcherlive-config

Implement a hot-reload configuration system that detects file changes and notifies subscribers.

Python
import json
import os
import time
from typing import Callable

class HotReloadConfig:
    def __init__(self, config_path: str):
        self._path = config_path
        self._data: dict = {}
        self._last_mtime: float = 0.0
        self._subscribers: list[Callable[[str, object, object], None]] = []
        self._load()

    def _load(self) -> bool:
        try:
            mtime = os.path.getmtime(self._path)
            if mtime == self._last_mtime:
                return False
            with open(self._path) as f:
                new_data = json.load(f)
            old_data = self._data
            self._data = new_data
            self._last_mtime = mtime
            # Notify subscribers of changes
            all_keys = set(old_data.keys()) | set(new_data.keys())
            for key in all_keys:
                if old_data.get(key) != new_data.get(key):
                    for cb in self._subscribers:
                        cb(key, old_data.get(key), new_data.get(key))
            return True
        except (FileNotFoundError, json.JSONDecodeError):
            return False

    def on_change(self, callback: Callable[[str, object, object], None]) -> None:
        self._subscribers.append(callback)

    def get(self, key: str, default=None):
        return self._data.get(key, default)

    def check_reload(self) -> bool:
        return self._load()

import tempfile

with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
    json.dump({"log_level": "INFO", "max_retries": 3}, f)
    config_path = f.name

cfg = HotReloadConfig(config_path)
print(f"Initial config loaded: log_level={cfg.get('log_level')}")

cfg.on_change(lambda key, old, new: print(f"Config changed! {key} changed: {old} -> {new}"))

# Simulate file change
time.sleep(0.01)
with open(config_path, "w") as f:
    json.dump({"log_level": "DEBUG", "max_retries": 5}, f)

cfg.check_reload()
print(f"New config: {cfg._data}")
os.unlink(config_path)
Solution

The solution is above. In production, replace polling with a file system watcher:

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class ConfigFileHandler(FileSystemEventHandler):
def __init__(self, config: HotReloadConfig):
self._config = config

def on_modified(self, event):
if event.src_path == self._config._path:
self._config.check_reload()

observer = Observer()
observer.schedule(ConfigFileHandler(cfg), path=os.path.dirname(config_path))
observer.start()

Use cases for hot-reload config:

  • Feature flags: enable/disable features without deployment
  • Log levels: increase verbosity during an incident, then reduce
  • Rate limits: adjust limits in response to load without restarting
import json
import os
import time
from dataclasses import dataclass
from typing import Callable

class HotReloadConfig:
    """Watches a config file and reloads it when it changes.
    Notifies subscribers when config changes.
    """
    pass
Expected Output
Initial config loaded: log_level=INFO\nConfig changed! log_level changed: INFO -> DEBUG\nNew config: {'log_level': 'DEBUG', 'max_retries': 5}
Hints

Hint 1: Track the file modification time with os.path.getmtime(). Poll periodically and reload if mtime changed.

Hint 2: Store subscriber callbacks in a list. On reload, compare old and new values and call subscribers for each changed key.

#6Typed Config with Pydantic-Style ValidationMedium
pydantic-settingstyped-configvalidation

Implement a BaseSettings class that reads typed configuration from environment variables, mirroring pydantic-settings behavior.

Python
import os
from typing import get_type_hints, Optional, Any
import json

class ConfigError(Exception):
    pass

class BaseSettings:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)

    def __init__(self, env: dict = None):
        src = env if env is not None else os.environ
        hints = get_type_hints(self.__class__)
        for field_name, field_type in hints.items():
            env_key = field_name.upper()
            raw = src.get(env_key)
            if raw is not None:
                try:
                    value = self._cast(raw, field_type, field_name)
                except (ValueError, TypeError) as e:
                    raise ConfigError(f"'{field_name}' expected {getattr(field_type, '__name__', str(field_type))}, got {raw!r}")
            else:
                default = getattr(self.__class__, field_name, None)
                value = default
            setattr(self, field_name, value)

    def _cast(self, raw: str, type_: Any, field_name: str) -> Any:
        if type_ == bool:
            if raw.lower() in ("true", "1", "yes"):
                return True
            if raw.lower() in ("false", "0", "no"):
                return False
            raise ValueError(f"Cannot convert {raw!r} to bool")
        if type_ == int:
            return int(raw)
        if type_ == float:
            return float(raw)
        if type_ == list or (hasattr(type_, "__origin__") and type_.__origin__ is list):
            return json.loads(raw) if raw.startswith("[") else [x.strip() for x in raw.split(",")]
        return raw  # str or unknown

class AppSettings(BaseSettings):
    app_name: str = "MyApp"
    debug: bool = False
    port: int = 8000
    allowed_hosts: list = None

# Test 1: valid env
env1 = {"APP_NAME": "MyAPI", "DEBUG": "true", "PORT": "8080", "ALLOWED_HOSTS": "localhost,127.0.0.1"}
s = AppSettings(env=env1)
print(f"AppSettings(app_name={s.app_name!r}, debug={s.debug}, port={s.port}, allowed_hosts={s.allowed_hosts})")

# Test 2: bad int
try:
    AppSettings(env={"PORT": "not_a_port", "APP_NAME": "X"})
except ConfigError as e:
    print(f"ConfigError: {e}")

# Test 3: bad bool
try:
    AppSettings(env={"DEBUG": "maybe", "APP_NAME": "X"})
except ConfigError as e:
    print(f"ConfigError: {e}")
Solution

The solution is above. In production, replace this with pydantic-settings:

from pydantic_settings import BaseSettings

class AppSettings(BaseSettings):
app_name: str = "MyApp"
debug: bool = False
port: int = 8000
allowed_hosts: list[str] = ["localhost"]

class Config:
env_file = ".env"
env_file_encoding = "utf-8"

settings = AppSettings() # automatically reads from env + .env file

Pydantic-settings handles nested models, secret strings, validators, JSON encoding/decoding, and .env file loading — all battle-tested.

import os
from typing import Optional

# Without using pydantic, implement a BaseSettings-like class that:
# - Reads field values from environment variables (uppercase field names)
# - Supports default values
# - Validates types using field type annotations
# - Raises ConfigError with a clear message for type mismatches
Expected Output
AppSettings(app_name='MyAPI', debug=True, port=8080, allowed_hosts=['localhost', '127.0.0.1'])\nConfigError: 'port' expected int, got 'not_a_port'\nConfigError: 'debug' expected bool, got 'maybe'
Hints

Hint 1: Use typing.get_type_hints() to get field annotations. For each field, check os.environ.get(name.upper()). Convert to the annotated type or use the default.

Hint 2: For bool fields, accept "true"/"1"/"yes" as True and "false"/"0"/"no" as False (case-insensitive).

#7Multi-Environment Config FactoryMedium
multi-envconfig-factoryenvironment-profiles

Build a multi-environment config factory that produces the correct config object based on APP_ENV.

Python
from dataclasses import dataclass
from typing import Optional
import os

@dataclass
class AppConfig:
    debug: bool
    database_url: str
    log_level: str
    allowed_hosts: list

def make_development_config(env: dict) -> AppConfig:
    return AppConfig(
        debug=True,
        database_url=env.get("DATABASE_URL", "sqlite:///dev.db"),
        log_level=env.get("LOG_LEVEL", "DEBUG"),
        allowed_hosts=["localhost", "127.0.0.1"],
    )

def make_staging_config(env: dict) -> AppConfig:
    return AppConfig(
        debug=False,
        database_url=env.get("DATABASE_URL", "postgresql://staging-db/app"),
        log_level=env.get("LOG_LEVEL", "INFO"),
        allowed_hosts=["staging.example.com"],
    )

def make_production_config(env: dict) -> AppConfig:
    return AppConfig(
        debug=False,
        database_url=env["DATABASE_URL"],  # required in production
        log_level=env.get("LOG_LEVEL", "WARNING"),
        allowed_hosts=env.get("ALLOWED_HOSTS", "example.com").split(","),
    )

_FACTORIES = {
    "development": make_development_config,
    "staging": make_staging_config,
    "production": make_production_config,
}

def create_config(env: dict = None) -> AppConfig:
    src = env if env is not None else os.environ
    app_env = src.get("APP_ENV", "development").lower()
    if app_env not in _FACTORIES:
        raise ValueError(f"Unknown APP_ENV: {app_env!r}. Valid: {list(_FACTORIES.keys())}")
    return _FACTORIES[app_env](src)

# Test
dev_cfg = create_config({"APP_ENV": "development"})
print(f"Development: debug={dev_cfg.debug}, db={dev_cfg.database_url}, log={dev_cfg.log_level}")

prod_cfg = create_config({"APP_ENV": "production", "DATABASE_URL": "postgresql://prod"})
print(f"Production: debug={prod_cfg.debug}, db={prod_cfg.database_url}, log={prod_cfg.log_level}")

try:
    create_config({"APP_ENV": "unknown"})
except ValueError:
    print("Unknown env error caught")
Solution

The solution is above. The factory pattern cleanly separates per-environment defaults from the config structure.

Key production principle: The production factory should have NO defaults for secrets — it requires DATABASE_URL explicitly. This catches misconfiguration at startup, not when the first DB query fails.

Testing benefit: You can test all environment configs in unit tests:

def test_production_requires_database_url():
with pytest.raises(KeyError):
create_config({"APP_ENV": "production"}) # no DATABASE_URL

def test_development_defaults():
cfg = create_config({"APP_ENV": "development"})
assert cfg.debug is True
assert "sqlite" in cfg.database_url
from dataclasses import dataclass
from typing import Optional
import os

# Implement a config factory that produces different configs
# based on APP_ENV (development, staging, production).
# Each environment has different defaults but all read from env vars.
# Fail clearly if APP_ENV is unrecognized.
Expected Output
Development: debug=True, db=sqlite:///dev.db, log=DEBUG\nProduction: debug=False, db=postgresql://prod, log=WARNING\nUnknown env error caught
Hints

Hint 1: Define a base config dataclass and subclass or override values for each environment. A factory function reads APP_ENV and returns the appropriate config.

Hint 2: Alternatively, use a dict mapping env name to a factory function that creates the config.


Hard

#8Config Schema Documentation GeneratorHard
schema-docsintrospectionconfig-documentation

Build a config documentation generator that reads a dataclass and produces a markdown reference table.

Python
from dataclasses import dataclass, field, fields as dc_fields
from typing import Any, Optional
import inspect

def generate_config_docs(cls) -> str:
    lines = [
        f"## Configuration Reference: `{cls.__name__}`",
        "",
        "| Key | Type | Required | Default | Description |",
        "|-----|------|----------|---------|-------------|",
    ]

    hints = {}
    for base in reversed(cls.__mro__):
        if hasattr(base, "__annotations__"):
            hints.update(base.__annotations__)

    for f in dc_fields(cls):
        type_name = hints.get(f.name, f.type)
        if hasattr(type_name, "__name__"):
            type_str = type_name.__name__
        else:
            type_str = str(type_name).replace("typing.", "")

        has_default = f.default is not f.default_factory or f.default_factory is not f.default_factory
        if f.default is not inspect.Parameter.empty and str(f.default) != "MISSING":
            required = "No"
            default = f"`{f.default}`"
        elif f.default_factory is not inspect.Parameter.empty and str(f.default_factory) != "MISSING":  # type: ignore
            required = "No"
            default = f"*factory*"
        else:
            required = "**Yes**"
            default = "—"

        description = f.metadata.get("description", "") if f.metadata else ""
        lines.append(f"| `{f.name}` | `{type_str}` | {required} | {default} | {description} |")

    return "\n".join(lines)

@dataclass
class ServerConfig:
    host: str = field(default="0.0.0.0", metadata={"description": "Bind address for the HTTP server"})
    port: int = field(default=8000, metadata={"description": "TCP port to listen on (1-65535)"})
    workers: int = field(default=4, metadata={"description": "Number of worker processes"})
    debug: bool = field(default=False, metadata={"description": "Enable debug mode (never use in production)"})
    database_url: str = field(default="MISSING", metadata={"description": "PostgreSQL connection URL (required)"})

print(generate_config_docs(ServerConfig))
Solution

The solution above generates documentation directly from code, keeping docs and implementation in sync.

Automating docs in CI:

# scripts/gen_config_docs.py
from myapp.config import AppConfig, DatabaseConfig, CacheConfig
from pathlib import Path

docs = "\n\n".join([
generate_config_docs(AppConfig),
generate_config_docs(DatabaseConfig),
generate_config_docs(CacheConfig),
])
Path("docs/configuration.md").write_text(docs)

Run this in CI — if the docs drift from the code, the CI check fails. Alternatively, use pytest to assert the generated docs match a committed file.

from dataclasses import dataclass, fields
from typing import Any, Optional

# Build a documentation generator that:
# - Reads a config class's field annotations and docstrings
# - Outputs a markdown table documenting each config key
# - Shows: name, type, required/optional, default, description
Expected Output
See solution for markdown table output
Hints

Hint 1: Use dataclasses.fields() to get all fields. Each FieldInfo has .name, .type, .default, .default_factory.

Hint 2: For description, use the field metadata dict: field(default=..., metadata={"description": "..."})

#9Config Diff and Migration ToolHard
config-diffmigrationbackward-compatibility

Implement a config diff and migration tool that upgrades config dicts between schema versions.

Python
from typing import Any
from dataclasses import dataclass

@dataclass
class ConfigVersion:
    version: str
    schema: dict[str, Any]  # key -> default value

def diff_schemas(old: ConfigVersion, new: ConfigVersion) -> dict:
    old_keys = set(old.schema.keys())
    new_keys = set(new.schema.keys())

    added = list(new_keys - old_keys)
    removed = list(old_keys - new_keys)
    changed_defaults = {
        k: (old.schema[k], new.schema[k])
        for k in old_keys & new_keys
        if old.schema[k] != new.schema[k]
    }
    return {"added": added, "removed": removed, "changed_defaults": changed_defaults}

def migrate_config(old_config: dict, old: ConfigVersion, new: ConfigVersion) -> tuple[dict, list[str]]:
    warnings = []
    result = dict(old_config)

    diff = diff_schemas(old, new)

    # Remove keys no longer in schema
    for key in diff["removed"]:
        if key in result:
            warnings.append(f"Warning: '{key}' was set but is no longer used")
            del result[key]

    # Add new keys with defaults
    for key in diff["added"]:
        if key not in result:
            result[key] = new.schema[key]

    return result, warnings

# Define schema versions
v1 = ConfigVersion("1.0", {
    "host": "localhost",
    "port": 5432,
    "workers": 4,
    "legacy_timeout": 30,
})

v2 = ConfigVersion("2.0", {
    "host": "localhost",
    "port": 5432,
    "workers": 8,       # default changed
    "cache_ttl": 60,    # new
    "max_retries": 3,   # new
    # legacy_timeout removed
})

diff = diff_schemas(v1, v2)
print(f"Added keys: {sorted(diff['added'])}")
print(f"Removed keys: {diff['removed']}")
print(f"Changed defaults: {diff['changed_defaults']}")

old_config = {"host": "myhost", "port": 5432, "workers": 4, "legacy_timeout": 45}
migrated, warnings = migrate_config(old_config, v1, v2)
print(f"Migrated config: {migrated}")
for w in warnings:
    print(w)
Solution

The solution is above. Config migration tools are critical for long-running services where config files accumulate over many releases.

Automated migration in startup:

def load_and_migrate_config(config_path: str) -> dict:
with open(config_path) as f:
raw = json.load(f)

config_version = raw.pop("_schema_version", "1.0")
migrations = get_migrations_from(config_version, CURRENT_VERSION)

result = raw
for old_v, new_v in migrations:
result, warnings = migrate_config(result, old_v, new_v)
for w in warnings:
logging.warning(w)

return result
from dataclasses import dataclass
from typing import Any

# Implement a config migration tool that:
# 1. Detects keys added, removed, or changed between two config versions
# 2. Migrates an old config dict to the new schema
# 3. Warns about removed keys that were set
# 4. Fills defaults for new required keys
Expected Output
Added keys: ['cache_ttl', 'max_retries']\nRemoved keys: ['legacy_timeout']\nChanged defaults: {'workers': (4, 8)}\nMigrated config: {'host': 'myhost', 'port': 5432, 'workers': 4, 'cache_ttl': 60, 'max_retries': 3}\nWarning: 'legacy_timeout' was set but is no longer used
Hints

Hint 1: Define old_schema and new_schema as dicts mapping key name to default value. Diff them to find added/removed/changed keys.

Hint 2: Migration: start with old_config, remove removed keys (with warning if set), add new keys with defaults, keep existing keys.

#10Secrets Manager IntegrationHard
secretsconfigsecurity

Build a secrets management layer that keeps secret values out of logs and supports rotation.

Python
from typing import Callable, Any, Optional
from dataclasses import dataclass

class Secret:
    """A wrapper that prevents accidental secret logging."""
    def __init__(self, value: str, name: str = ""):
        self._value = value
        self._name = name

    def get(self) -> str:
        return self._value

    def __repr__(self) -> str:
        return f"Secret({self._name!r}: REDACTED)"

    def __str__(self) -> str:
        return "REDACTED"

    def __eq__(self, other) -> bool:
        if isinstance(other, Secret):
            return self._value == other._value
        return False

class SecretsManager:
    def __init__(self):
        self._resolvers: dict[str, Callable[[], str]] = {}
        self._cache: dict[str, Secret] = {}

    def register(self, name: str, resolver: Callable[[], str]) -> None:
        self._resolvers[name] = resolver
        # Invalidate cache on re-registration
        self._cache.pop(name, None)

    def get(self, name: str) -> Secret:
        if name not in self._cache:
            if name not in self._resolvers:
                raise KeyError(f"No secret registered: {name}")
            value = self._resolvers[name]()
            self._cache[name] = Secret(value, name)
        return self._cache[name]

    def refresh(self, name: str) -> None:
        """Force re-resolution of a secret (for rotation)."""
        self._cache.pop(name, None)
        value = self._resolvers[name]()
        self._cache[name] = Secret(value, name)
        print(f"Secret refreshed: {name}")

@dataclass
class AppConfig:
    db_host: str
    db_port: int

def make_env_resolver(key: str, env: dict):
    return lambda: env.get(key, "")

# Set up
fake_env = {"DB_PASSWORD": "super_secret_pw", "API_KEY": "sk-your-key-here"}
mgr = SecretsManager()
mgr.register("DB_PASSWORD", make_env_resolver("DB_PASSWORD", fake_env))
mgr.register("API_KEY", make_env_resolver("API_KEY", fake_env))

# Secrets are redacted in output
db_pass = mgr.get("DB_PASSWORD")
api_key = mgr.get("API_KEY")
print(f"DB password: {db_pass}")
print(f"API key: {api_key}")

# Config values (non-secret) are loggable
app_cfg = AppConfig(db_host="localhost", db_port=5432)
import dataclasses
print(f"Config (safe to log): {dataclasses.asdict(app_cfg)}")

# Simulate secret rotation
fake_env["DB_PASSWORD"] = "new_rotated_pw"
mgr.refresh("DB_PASSWORD")
Solution

The solution is above. The Secret class is the key: even if code accidentally logs the secret object, it shows REDACTED instead of the real value. Only secret.get() returns the real value — and code reviews can search for .get() calls to audit secret usage.

Production vault integration:

import hvac # HashiCorp Vault client

def vault_resolver(path: str, key: str, vault_client) -> Callable[[], str]:
def resolve() -> str:
secret = vault_client.secrets.kv.read_secret_version(path=path)
return secret["data"]["data"][key]
return resolve

mgr.register("DB_PASSWORD", vault_resolver("database/prod", "password", vault_client))

The resolver pattern means the SecretsManager is backend-agnostic — env vars, files, Vault, AWS Secrets Manager, GCP Secret Manager are all just different resolver callables.

import os
from typing import Optional, Callable
from dataclasses import dataclass

# Build a secrets management layer that:
# - Resolves secret values from multiple backends (env, file, in-memory vault)
# - Redacts secrets in logs (never prints raw secret values)
# - Supports secret rotation (update without restart)
# - Distinguishes config values (loggable) from secrets (redacted)
Expected Output
DB password: REDACTED\nAPI key: REDACTED\nConfig (safe to log): {'db_host': 'localhost', 'db_port': 5432}\nSecret refreshed: DB_PASSWORD
Hints

Hint 1: Create a Secret class that stores the value but never shows it in __repr__ or __str__. Implement get() to retrieve the raw value when needed.

Hint 2: The SecretsManager holds a dict of name -> resolver callable. Calling resolve() runs the callable and wraps the result in a Secret.

#11Complete 12-Factor Config SystemHard
12-factorfull-config-systemproduction-config

Build a complete 12-factor compliant configuration system combining environment reading, secrets management, validation, and multi-environment support.

Python
import os
from typing import Any, Callable, Optional
from dataclasses import dataclass

# ── SECRET WRAPPER ────────────────────────────────────────────────────────────

class Secret:
    def __init__(self, value: str, name: str = ""):
        self._value = value
        self._name = name
    def get(self) -> str: return self._value
    def __str__(self) -> str: return "REDACTED"
    def __repr__(self) -> str: return f"Secret({self._name!r})"

# ── CONFIG ERROR ──────────────────────────────────────────────────────────────

class ConfigError(Exception):
    pass

# ── CONFIG READER ─────────────────────────────────────────────────────────────

class ConfigReader:
    def __init__(self, env: dict = None):
        self._env = env if env is not None else os.environ
        self._errors: list[str] = []

    def require(self, key: str, cast=str) -> Any:
        val = self._env.get(key)
        if val is None:
            self._errors.append(f"Missing required: {key}")
            return None
        try:
            return cast(val)
        except (ValueError, TypeError):
            self._errors.append(f"Invalid value for {key}: {val!r}")
            return None

    def optional(self, key: str, default: Any, cast=str) -> Any:
        val = self._env.get(key)
        if val is None:
            return default
        try:
            return cast(val)
        except (ValueError, TypeError):
            return default

    def secret(self, key: str) -> Optional[Secret]:
        val = self._env.get(key)
        if val is None:
            self._errors.append(f"Missing secret: {key}")
            return None
        return Secret(val, key)

    def validate(self) -> None:
        if self._errors:
            raise ConfigError("Configuration errors:\n" + "\n".join(f"  - {e}" for e in self._errors))

# ── CONFIG OBJECT ─────────────────────────────────────────────────────────────

@dataclass
class AppConfig:
    app_env: str
    debug: bool
    db_host: str
    db_port: int
    db_password: Optional[Secret]
    log_level: str
    allowed_origins: list[str]

    def introspect(self) -> dict:
        return {
            "app_env": self.app_env,
            "debug": self.debug,
            "db_host": self.db_host,
            "db_port": self.db_port,
            "db_password": str(self.db_password) if self.db_password else None,
            "log_level": self.log_level,
            "allowed_origins": self.allowed_origins,
        }

# ── FACTORY ───────────────────────────────────────────────────────────────────

def load_config(env: dict = None) -> AppConfig:
    reader = ConfigReader(env)

    app_env = reader.optional("APP_ENV", "development")
    debug = reader.optional("DEBUG", False, lambda v: v.lower() == "true")
    db_host = reader.optional("DB_HOST", "localhost")
    db_port = reader.optional("DB_PORT", 5432, int)
    log_level = reader.optional("LOG_LEVEL", "INFO" if app_env != "development" else "DEBUG")
    origins_raw = reader.optional("ALLOWED_ORIGINS", "localhost")
    allowed_origins = [o.strip() for o in origins_raw.split(",")]

    # Secrets required in production
    if app_env == "production":
        db_password = reader.secret("DB_PASSWORD")
    else:
        raw = env.get("DB_PASSWORD", "dev_password") if env else "dev_password"
        db_password = Secret(raw, "DB_PASSWORD")

    reader.validate()  # raises ConfigError if any required keys missing

    return AppConfig(
        app_env=app_env,
        debug=debug,
        db_host=db_host,
        db_port=db_port,
        db_password=db_password,
        log_level=log_level,
        allowed_origins=allowed_origins,
    )

# ── TEST USAGE ────────────────────────────────────────────────────────────────

# Development config
dev_cfg = load_config({"APP_ENV": "development", "DB_HOST": "localhost"})
print(f"Dev env: {dev_cfg.app_env}, debug={dev_cfg.debug}, log={dev_cfg.log_level}")
print(f"Dev introspect: {dev_cfg.introspect()}")

# Production config
prod_cfg = load_config({
    "APP_ENV": "production",
    "DB_HOST": "db.prod.example.com",
    "DB_PORT": "5432",
    "DB_PASSWORD": "s3cr3t_prod_pw",
    "ALLOWED_ORIGINS": "example.com,api.example.com",
})
print(f"\nProd env: {prod_cfg.app_env}, debug={prod_cfg.debug}")
print(f"Prod introspect: {prod_cfg.introspect()}")

# Missing required secret in production
try:
    load_config({"APP_ENV": "production", "DB_HOST": "db.prod.example.com"})
except ConfigError as e:
    print(f"\nConfig validation failed: {str(e)[:60]}...")
Solution

The solution implements all key 12-factor config principles:

Factor II — Explicitly declared dependencies: Config is declared as typed fields in AppConfig. All required keys are explicit.

Factor III — Config in the environment: ConfigReader reads from os.environ. No config is stored in files that ship with the code.

Fail fast: reader.validate() collects ALL errors and raises once, showing every missing key at startup — not one by one as each component tries to use them.

Secrets separate from config: Secret objects never appear in logs. Non-secret config values are freely loggable.

Test injection: Pass a dict to load_config(env=...) to override environment variables without os.environ manipulation.

The introspect() method is safe to call in monitoring endpoints or startup logs — it shows what is configured without leaking secrets.

# Build a complete 12-factor compliant config system:
# - All config from environment (Factor III)
# - Strict separation of dev/staging/prod (Factor II)
# - Fail fast on missing required config (startup validation)
# - Secrets managed separately from non-sensitive config
# - Config introspection: show what's configured (with secrets redacted)
# - Config test mode: allow dict injection for tests
Expected Output
See solution for expected output
Hints

Hint 1: Build on the BaseSettings, SecretsManager, and environment factory patterns from earlier problems.

Hint 2: The introspect() method should return a dict safe for logging — replacing Secret values with REDACTED.

© 2026 EngineersOfAI. All rights reserved.