Migration Guide

This guide helps you migrate from other configuration management libraries to Confii.


Table of Contents


Why Migrate?

What You Gain

Capability

What it means

Unified multi-source loading

YAML, JSON, TOML, INI, env vars, S3, SSM, Azure Blob, GCP Storage, Git β€” one API

Config[T] typed access

Full IDE autocomplete and mypy/pyright checking via .typed property

Secret store integration

AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, HashiCorp Vault (10 auth methods)

Source tracking

config.get_source("database.host") tells you which file set each value

Hot reload

File watcher detects changes and reloads automatically

Composition

_include and _defaults directives for composing configs from fragments

Config diffing

Compare two config states programmatically

What You Keep

  • Pydantic models work unchanged. Pass your existing BaseModel as schema= and it just works.

  • YAML/JSON/TOML files are compatible. No format conversion needed for standard files.

  • Environment variables still work. EnvironmentLoader reads them with prefix-based nesting.

What’s Different (Tradeoffs)

Area

Detail

No variable interpolation

Confii does not support ${db.host}-style interpolation within config values. Use ${ENV_VAR} expansion for environment variables instead.

No multirun/sweep

Hydra’s experiment sweep feature has no equivalent. Use an external orchestrator.

No instantiate()

Hydra’s hydra.utils.instantiate() for object creation is not provided.

Learning curve

If your team is deeply invested in another tool, migration has a cost. Evaluate whether the added capabilities justify it.


Migration from python-dotenv

Before (python-dotenv)

from dotenv import load_dotenv
import os

load_dotenv()

DATABASE_HOST = os.getenv("DATABASE_HOST", "localhost")
DATABASE_PORT = int(os.getenv("DATABASE_PORT", "5432"))
DEBUG = os.getenv("DEBUG", "false").lower() == "true"

After (Confii) β€” Untyped

from confii import Config
from confii.loaders import EnvironmentLoader

config = Config(loaders=[EnvironmentLoader("APP")])

# Attribute access with automatic type casting
database_host = config.database.host
database_port = config.database.port  # auto-cast to int

After (Confii) β€” Typed with Config[T]

from pydantic import BaseModel
from confii import Config
from confii.loaders import EnvironmentLoader, YamlLoader

class AppConfig(BaseModel):
    database_host: str = "localhost"
    database_port: int = 5432
    debug: bool = False

config = Config[AppConfig](
    loaders=[
        YamlLoader("config.yaml"),       # base config file
        EnvironmentLoader("APP"),         # env vars override file values
    ],
    schema=AppConfig,
    validate_on_load=True,
)

# Full IDE autocomplete β€” your editor knows the types
host = config.typed.database_host   # IDE knows: str
port = config.typed.database_port   # IDE knows: int
debug = config.typed.debug          # IDE knows: bool

Step-by-Step Checklist

  1. Install Confii: pip install confii

  2. Convert your .env file to YAML (or keep it and use EnvironmentLoader)

  3. Replace os.getenv() calls with config.key attribute access

  4. Remove load_dotenv() calls

  5. (Optional) Define a Pydantic model for typed access

  6. Remove python-dotenv from your dependencies

CLI Migration

confii migrate dotenv .env --output config.yaml

This converts KEY=value pairs to a flat YAML file. Nested keys using __ separators (e.g., DATABASE__HOST) are converted to nested YAML structures.


Migration from python-decouple

Before (python-decouple)

from decouple import config, Csv

DATABASE_HOST = config("DATABASE_HOST", default="localhost")
DATABASE_PORT = config("DATABASE_PORT", default=5432, cast=int)
ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv())

After (Confii) β€” Untyped

from confii import Config
from confii.loaders import EnvironmentLoader

config = Config(loaders=[EnvironmentLoader("APP")])

# Type casting is automatic β€” no cast= parameter needed
database_host = config.database.host
database_port = config.database.port

After (Confii) β€” Typed with Config[T]

from typing import List
from pydantic import BaseModel
from confii import Config
from confii.loaders import EnvironmentLoader

class AppConfig(BaseModel):
    database_host: str = "localhost"
    database_port: int = 5432
    allowed_hosts: List[str] = ["localhost"]

config = Config[AppConfig](
    loaders=[EnvironmentLoader("APP")],
    schema=AppConfig,
    validate_on_load=True,
)

hosts = config.typed.allowed_hosts  # IDE knows: List[str]

Step-by-Step Checklist

  1. Install Confii: pip install confii

  2. Replace from decouple import config with Confii setup

  3. Remove cast= arguments β€” type casting is automatic, or use a Pydantic schema

  4. Replace config("KEY") calls with config.key attribute access

  5. Remove python-decouple from your dependencies


Migration from OmegaConf

OmegaConf is widely used in ML/data science pipelines, often alongside Hydra. This section covers standalone OmegaConf usage.

Loading YAML files

Before (OmegaConf):

from omegaconf import OmegaConf

cfg = OmegaConf.load("config.yaml")
host = cfg.database.host

After (Confii):

from confii import Config
from confii.loaders import YamlLoader

config = Config(loaders=[YamlLoader("config.yaml")])
host = config.database.host

Merging configs

Before (OmegaConf):

from omegaconf import OmegaConf

base = OmegaConf.load("base.yaml")
overrides = OmegaConf.load("overrides.yaml")
cfg = OmegaConf.merge(base, overrides)

After (Confii):

from confii import Config
from confii.loaders import YamlLoader

# Later loaders override earlier loaders (deep merge by default)
config = Config(loaders=[
    YamlLoader("base.yaml"),
    YamlLoader("overrides.yaml"),
])

Confii deep-merges by default. For more control, use merge_strategy and merge_strategy_map:

from confii import Config
from confii.loaders import YamlLoader
from confii.merge_strategies import MergeStrategy

config = Config(
    loaders=[YamlLoader("base.yaml"), YamlLoader("overrides.yaml")],
    merge_strategy=MergeStrategy.DEEP_MERGE,
    merge_strategy_map={
        "database.replicas": MergeStrategy.REPLACE,  # replace list instead of merging
    },
)

Converting to plain dict

Before (OmegaConf):

plain = OmegaConf.to_container(cfg, resolve=True)

After (Confii):

plain = config.to_dict()

Variable interpolation

OmegaConf supports ${db.host} references inside config values. Confii does not have cross-key interpolation. It does support environment variable expansion:

# OmegaConf style (NOT supported):
url: "http://${db.host}:${db.port}/mydb"

# Confii style (environment variable expansion):
url: "http://${DB_HOST}:${DB_PORT}/mydb"

If you rely heavily on cross-key interpolation, you have two options:

  1. Pre-resolve interpolation before loading (e.g., with a build step)

  2. Use environment variables to share values across config keys

Structured configs β†’ Config[T]

Before (OmegaConf structured configs):

from dataclasses import dataclass
from omegaconf import OmegaConf, MISSING

@dataclass
class DBConfig:
    host: str = MISSING
    port: int = 5432

@dataclass
class AppConfig:
    db: DBConfig = DBConfig()

cfg = OmegaConf.structured(AppConfig)
cfg.merge_with(OmegaConf.load("config.yaml"))

After (Confii with Pydantic):

from pydantic import BaseModel
from confii import Config
from confii.loaders import YamlLoader

class DBConfig(BaseModel):
    host: str
    port: int = 5432

class AppConfig(BaseModel):
    db: DBConfig

config = Config[AppConfig](
    loaders=[YamlLoader("config.yaml")],
    schema=AppConfig,
    validate_on_load=True,
)

config.typed.db.host   # IDE knows: str
config.typed.db.port   # IDE knows: int

Step-by-Step Checklist

  1. Install Confii: pip install confii

  2. Replace OmegaConf.load() with YamlLoader

  3. Replace OmegaConf.merge() with multiple loaders (later overrides earlier)

  4. Replace OmegaConf.to_container() with config.to_dict()

  5. Replace @dataclass structured configs with Pydantic BaseModel + Config[T]

  6. Refactor any ${key.ref} interpolation to use env vars or pre-resolve

  7. Remove omegaconf from your dependencies


Migration from Dynaconf

Basic usage

Before (Dynaconf):

from dynaconf import Settings

settings = Settings(
    ENV_FOR_DYNACONF="production",
    SETTINGS_FILE_FOR_DYNACONF=["settings.yaml", "production.yaml"],
)

database_host = settings.DATABASE.HOST

After (Confii) β€” Untyped:

from confii import Config
from confii.loaders import YamlLoader

config = Config(
    env="production",
    loaders=[
        YamlLoader("settings.yaml"),
        YamlLoader("production.yaml"),
    ],
)

database_host = config.database.host

Environment switching

Before (Dynaconf):

from dynaconf import Settings

settings = Settings()

# Switch environment at runtime
settings.from_env("production")

# Or use decorator
@settings.use_env("production")
def get_db_url():
    return settings.DATABASE_URL

After (Confii):

from confii import Config
from confii.loaders import YamlLoader

# Set environment at construction time
config = Config(env="production", loaders=[YamlLoader("settings.yaml")])

# Or load env vars with a prefix
config = Config(env_prefix="MYAPP", loaders=[YamlLoader("settings.yaml")])

Confii sets the environment at construction time. If you need multiple environments simultaneously, create multiple Config instances.

Dynaconf Vault integration β†’ Confii secret stores

Before (Dynaconf):

# settings.yaml
# VAULT_ENABLED_FOR_DYNACONF: true
# VAULT_URL_FOR_DYNACONF: https://vault.example.com

from dynaconf import Settings
settings = Settings()
db_password = settings.DATABASE_PASSWORD  # reads from Vault

After (Confii):

from confii import Config
from confii.loaders import YamlLoader
from confii.secret_stores import HashiCorpVault, SecretResolver

vault = HashiCorpVault(
    url="https://vault.example.com",
    auth_method="approle",             # or token, kubernetes, ldap, oidc, aws, azure, gcp, jwt
    role_id="my-role-id",
    secret_id="my-secret-id",
)

config = Config(
    loaders=[YamlLoader("config.yaml")],
    secret_resolver=SecretResolver(vault),
)

Confii supports 10 Vault auth methods (token, AppRole, Kubernetes, LDAP, OIDC, AWS IAM, Azure, GCP, JWT, userpass) compared to Dynaconf’s token-based auth. It also supports AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, and multi-store fallback via MultiSecretStore.

In your config files, reference secrets with the ${secret:path} syntax:

database:
  password: "${secret:db/password}"
  api_key: "${secret:services/api-key}"

Dynaconf validators β†’ Pydantic with Config[T]

Before (Dynaconf):

from dynaconf import Settings, Validator

settings = Settings(
    validators=[
        Validator("DATABASE.HOST", must_exist=True),
        Validator("DATABASE.PORT", gte=1024, lte=65535),
    ]
)

After (Confii):

from pydantic import BaseModel, Field
from confii import Config
from confii.loaders import YamlLoader

class DatabaseConfig(BaseModel):
    host: str                                          # required (must exist)
    port: int = Field(ge=1024, le=65535, default=5432) # range validation

class AppConfig(BaseModel):
    database: DatabaseConfig

config = Config[AppConfig](
    loaders=[YamlLoader("settings.yaml")],
    schema=AppConfig,
    validate_on_load=True,
    strict_validation=True,  # raise on validation failure
)

config.typed.database.host  # IDE knows: str
config.typed.database.port  # IDE knows: int

Pydantic gives you richer validation (regex patterns, custom validators, nested models, union types) than Dynaconf’s built-in validators.

Step-by-Step Checklist

  1. Install Confii: pip install confii

  2. Replace from dynaconf import Settings with from confii import Config

  3. Convert Settings(ENV_FOR_DYNACONF="x") to Config(env="x")

  4. Convert SETTINGS_FILE_FOR_DYNACONF=["a.yaml"] to loaders=[YamlLoader("a.yaml")]

  5. Replace settings.KEY with config.key (Confii is case-sensitive by default)

  6. If using Dynaconf Vault: set up HashiCorpVault + SecretResolver

  7. If using Dynaconf validators: convert to a Pydantic BaseModel

  8. Remove Dynaconf env vars (*_FOR_DYNACONF) from your environment

  9. Remove dynaconf from your dependencies

CLI Migration

confii migrate dynaconf settings.yaml --output config.yaml

This parses the YAML (or JSON) file and outputs a clean Confii-compatible file. You will still need to manually update your Python code.


Migration from Hydra

Removing the @hydra.main() decorator

Before (Hydra):

import hydra
from omegaconf import DictConfig

@hydra.main(config_path="conf", config_name="config", version_base=None)
def main(cfg: DictConfig) -> None:
    print(cfg.database.host)

if __name__ == "__main__":
    main()

After (Confii):

from confii import Config
from confii.loaders import YamlLoader

def main() -> None:
    config = Config(loaders=[YamlLoader("conf/config.yaml")])
    print(config.database.host)

if __name__ == "__main__":
    main()

No decorator, no special entry point, no working directory changes. Config is just a regular object you create when you need it.

Config groups β†’ _include composition

Before (Hydra) β€” conf/config.yaml:

defaults:
  - database: postgres
  - server: nginx

app:
  name: myapp

With separate files conf/database/postgres.yaml and conf/server/nginx.yaml.

After (Confii) β€” config.yaml:

_include:
  - conf/database/postgres.yaml
  - conf/server/nginx.yaml

app:
  name: myapp

Or using _defaults:

_defaults:
  - database: postgres
  - server: nginx

app:
  name: myapp

Confii processes _include and _defaults directives to compose configs from multiple fragments, with cycle detection for recursive includes.

defaults: list β†’ _defaults: directive

The mapping is straightforward β€” rename the key:

Hydra

Confii

defaults:

_defaults:

- database: postgres

- database: postgres

- override database: mysql

Not supported β€” use a separate override loader

hydra.utils.instantiate() β€” No equivalent

Hydra can instantiate Python objects from config:

model:
  _target_: torch.nn.Linear
  in_features: 128
  out_features: 10

Confii does not provide object instantiation from config. This is a deliberate design choice β€” config loading and object construction are separate concerns. If you need this pattern, instantiate objects in your application code:

from confii import Config
from confii.loaders import YamlLoader
import torch.nn

config = Config(loaders=[YamlLoader("config.yaml")])

# Instantiate manually
model = torch.nn.Linear(
    in_features=config.model.in_features,
    out_features=config.model.out_features,
)

Multirun/sweep β€” No equivalent

Hydra’s --multirun feature for hyperparameter sweeps:

python train.py --multirun learning_rate=0.001,0.01,0.1

Confii does not provide experiment sweep functionality. Use an external tool (Optuna, Weights & Biases, Ray Tune) for hyperparameter sweeps and pass config values via environment variables or separate config files.

Structured configs β†’ Config[T]

Before (Hydra + OmegaConf):

from dataclasses import dataclass
from hydra.core.config_store import ConfigStore

@dataclass
class DBConfig:
    host: str = "localhost"
    port: int = 5432

cs = ConfigStore.instance()
cs.store(name="db", node=DBConfig)

After (Confii):

from pydantic import BaseModel
from confii import Config
from confii.loaders import YamlLoader

class DBConfig(BaseModel):
    host: str = "localhost"
    port: int = 5432

class AppConfig(BaseModel):
    db: DBConfig = DBConfig()

config = Config[AppConfig](
    loaders=[YamlLoader("config.yaml")],
    schema=AppConfig,
    validate_on_load=True,
)

config.typed.db.host   # IDE knows: str
config.typed.db.port   # IDE knows: int

Step-by-Step Checklist

  1. Install Confii: pip install confii

  2. Remove @hydra.main() decorator from your entry point

  3. Replace initialize() / compose() with Config(loaders=[...])

  4. Rename defaults: to _defaults: in your YAML files (or use _include:)

  5. Replace cfg.key with config.key (same attribute access pattern)

  6. Convert @dataclass structured configs to Pydantic BaseModel (optional)

  7. Replace hydra.utils.instantiate() with manual object construction

  8. Replace multirun/sweep with an external tool

  9. Remove hydra-core and omegaconf from your dependencies

CLI Migration

confii migrate hydra conf/config.yaml --output config.yaml

This strips Hydra-specific keys (defaults, hydra) from the YAML and outputs a clean file. Config group references are not auto-resolved β€” you will need to convert them to _include directives manually.


Migration from Pydantic Settings

The limitation of pydantic-settings

pydantic-settings validates config and gives you typed access, but it can only load from:

  • Environment variables

  • .env files

  • (with extras) Azure Key Vault, AWS Secrets Manager

It cannot load from YAML, JSON, TOML, INI, S3, SSM, Git repos, or HTTP endpoints.

Before (pydantic-settings)

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_host: str = "localhost"
    database_port: int = 5432
    redis_url: str = "redis://localhost:6379"

    class Config:
        env_prefix = "APP_"
        env_file = ".env"

settings = Settings()
host = settings.database_host   # IDE knows: str

After (Confii) β€” Same typing, more sources

from pydantic import BaseModel
from confii import Config
from confii.loaders import YamlLoader, EnvironmentLoader, SSMLoader

class Settings(BaseModel):
    database_host: str = "localhost"
    database_port: int = 5432
    redis_url: str = "redis://localhost:6379"

config = Config[Settings](
    loaders=[
        YamlLoader("config.yaml"),          # base config from file
        SSMLoader("/myapp/production"),      # AWS SSM Parameter Store
        EnvironmentLoader("APP"),            # env vars override everything
    ],
    schema=Settings,
    validate_on_load=True,
    strict_validation=True,
)

# Same typed access as pydantic-settings
host = config.typed.database_host   # IDE knows: str
port = config.typed.database_port   # IDE knows: int
url = config.typed.redis_url        # IDE knows: str

The Config[T].typed property is the key feature: you get the same Pydantic-validated, IDE-aware typed access that pydantic-settings provides, but your config can come from any combination of sources.

What changes, what stays the same

pydantic-settings

Confii with Config[T]

Pydantic model

BaseSettings (subclass)

BaseModel (standard)

Typed access

settings.field

config.typed.field

Env var loading

Built-in

EnvironmentLoader("PREFIX")

File loading

.env only

YAML, JSON, TOML, INI

Cloud sources

Limited extras

S3, SSM, Azure Blob, GCP Storage, HTTP, Git

Secret stores

Limited extras

AWS SM, Azure KV, GCP SM, HashiCorp Vault (10 auth methods)

Hot reload

No

dynamic_reloading=True

Source tracking

No

config.get_source("key")

Step-by-Step Checklist

  1. Install Confii: pip install confii

  2. Change BaseSettings to BaseModel (remove class Config inner class)

  3. Create a Config[YourModel] instance with your loaders

  4. Replace settings.field with config.typed.field

  5. Replace env_prefix in class Config with EnvironmentLoader("PREFIX") or env_prefix="PREFIX"

  6. (Optional) Add file loaders, SSM, secret stores

  7. Remove pydantic-settings from your dependencies (keep pydantic)


Automated Migration (CLI)

Confii provides a CLI tool that converts config files from other formats:

# Convert .env to YAML
confii migrate dotenv .env --output config.yaml

# Convert Dynaconf settings file
confii migrate dynaconf settings.yaml --output config.yaml

# Convert Hydra config (strips Hydra-specific keys)
confii migrate hydra conf/config.yaml --output config.yaml

What the CLI does

  • dotenv/env: Parses KEY=value pairs and converts to YAML. Nested keys using __ separators (e.g., DATABASE__HOST=localhost) become nested YAML structures.

  • dynaconf: Parses the YAML/JSON settings file and outputs it in the target format. No structural changes needed for most files.

  • hydra: Parses the YAML config, strips defaults: and hydra: keys, and outputs the remaining config.

What the CLI does NOT do

  • It does not rewrite your Python code. You still need to update import statements and config access patterns manually.

  • It does not resolve Hydra config group references. _defaults entries need manual conversion to _include paths.

  • It does not migrate secrets or environment-specific logic.

Output format options

# Output as JSON instead of YAML
confii migrate dotenv .env --output config.json --target-format json

# Output as TOML
confii migrate dotenv .env --output config.toml --target-format toml

# Print to stdout (no --output flag)
confii migrate dotenv .env

Feature Comparison

Feature

python-dotenv

python-decouple

OmegaConf

Dynaconf

Hydra

pydantic-settings

Confii

Multiple file formats

-

-

YAML

YAML, JSON, TOML, INI

YAML

.env

YAML, JSON, TOML, INI

Environment variables

Load to os.environ

Read from env

-

Yes

Yes (via plugin)

Yes

Yes

Type casting

-

Manual cast=

Yes

Yes

Yes

Pydantic

Auto + Pydantic

Schema validation

-

-

Structured configs

Basic validators

Structured configs

Pydantic

Pydantic + JSON Schema

Typed IDE access

-

-

Limited

-

Via structured

Yes

Yes (Config[T].typed)

Hot reload

-

-

-

Yes

-

-

Yes

Source tracking

-

-

-

-

-

-

Yes

Secret stores

-

-

-

⚠️ Vault (token auth)

-

⚠️ Limited extras

AWS SM, Azure KV, GCP SM, Vault (10 auth methods)

Cloud config sources

-

-

-

⚠️ Redis, Vault

-

-

S3, SSM, Azure Blob, GCP Storage, HTTP, Git

Config composition

-

-

Merge

⚠️ Basic

Yes

-

_include, _defaults

Config diffing

-

-

-

-

-

-

Yes

Variable interpolation

-

-

${key.ref}

@format {this.key}

${key.ref}

-

${ENV_VAR} only

Multirun/sweep

-

-

-

-

Yes

-

-

Object instantiation

-

-

-

-

instantiate()

-

-

Legend: Yes = full support, ⚠️ = partial/limited support, - = not supported


Common Migration Patterns

Pattern 1: Environment variables only

Before:

import os
host = os.getenv("DB_HOST", "localhost")
port = int(os.getenv("DB_PORT", "5432"))

After:

from confii import Config
from confii.loaders import EnvironmentLoader

config = Config(loaders=[EnvironmentLoader("DB")])
host = config.host
port = config.port

Pattern 2: File + environment overrides

Before:

import json, os

with open("config.json") as f:
    file_config = json.load(f)

host = os.getenv("DB_HOST") or file_config.get("database", {}).get("host")

After:

from confii import Config
from confii.loaders import JsonLoader, EnvironmentLoader

config = Config(loaders=[
    JsonLoader("config.json"),
    EnvironmentLoader("DB"),    # env vars override file values
])
host = config.database.host

Pattern 3: Multiple environments

Before:

import json, os

env = os.getenv("ENV", "development")
if env == "production":
    config = json.load(open("prod.json"))
else:
    config = json.load(open("dev.json"))

After:

from confii import Config
from confii.loaders import YamlLoader

config = Config(
    env="production",
    loaders=[YamlLoader("config.yaml")],  # contains environment sections
)

Your config.yaml uses environment sections:

default:
  database:
    host: localhost
    port: 5432

production:
  database:
    host: db.prod.example.com
    port: 5432

Pattern 4: Secrets from Vault + config from files

from confii import Config
from confii.loaders import YamlLoader, EnvironmentLoader
from confii.secret_stores import HashiCorpVault, SecretResolver

vault = HashiCorpVault(
    url="https://vault.example.com",
    auth_method="kubernetes",
    role="my-app",
)

config = Config(
    env="production",
    loaders=[
        YamlLoader("config.yaml"),
        EnvironmentLoader("APP"),
    ],
    secret_resolver=SecretResolver(vault),
)

# In config.yaml:
#   database:
#     password: "${secret:db/password}"
db_password = config.database.password  # resolved from Vault at load time

Pattern 5: Builder pattern for complex setups

from confii import Config
from confii.loaders import YamlLoader, EnvironmentLoader
from confii.secret_stores import AWSSecretsManager, SecretResolver

config = Config.builder() \
    .with_env("production") \
    .add_loader(YamlLoader("base.yaml")) \
    .add_loader(YamlLoader("production.yaml")) \
    .add_loader(EnvironmentLoader("APP")) \
    .with_secrets(SecretResolver(AWSSecretsManager(region_name="us-east-1"))) \
    .enable_dynamic_reloading() \
    .enable_debug() \
    .build()

Post-Migration Checklist

After migration, take advantage of Confii features:

1. Validate your configuration

confii validate production --loader yaml:config.yaml

2. Enable schema validation

config = Config(schema=MySettings, validate_on_load=True, strict_validation=True)

3. Set up hot reloading (if needed)

config = Config(dynamic_reloading=True, loaders=[YamlLoader("config.yaml")])

# Register a callback for config changes
config.on_change(lambda old, new: print(f"Config changed: {old} -> {new}"))

4. Integrate secret stores

from confii.secret_stores import AWSSecretsManager, SecretResolver

store = AWSSecretsManager(region_name="us-east-1")
config = Config(secret_resolver=SecretResolver(store))

5. Use the debug tools

# Lint config for issues
confii lint production --loader yaml:config.yaml

# Debug key resolution
confii debug production --key=database.host

# Explain where a value came from
confii explain production --key=database.host

6. Freeze config in production

config = Config(loaders=[YamlLoader("config.yaml")])
config.freeze()  # prevents set() and reload() β€” fully thread-safe

Getting Help

If you encounter issues during migration:

  1. Check the CLI debug tools (see above)

  2. Check the documentation for API details

  3. File an issue on GitHub with your before/after code and the error you see