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 |
|
Full IDE autocomplete and mypy/pyright checking via |
Secret store integration |
AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, HashiCorp Vault (10 auth methods) |
Source tracking |
|
Hot reload |
File watcher detects changes and reloads automatically |
Composition |
|
Config diffing |
Compare two config states programmatically |
What You Keepο
Pydantic models work unchanged. Pass your existing
BaseModelasschema=and it just works.YAML/JSON/TOML files are compatible. No format conversion needed for standard files.
Environment variables still work.
EnvironmentLoaderreads them with prefix-based nesting.
Whatβs Different (Tradeoffs)ο
Area |
Detail |
|---|---|
No variable interpolation |
Confii does not support |
No multirun/sweep |
Hydraβs experiment sweep feature has no equivalent. Use an external orchestrator. |
No |
Hydraβs |
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ο
Install Confii:
pip install confiiConvert your
.envfile to YAML (or keep it and useEnvironmentLoader)Replace
os.getenv()calls withconfig.keyattribute accessRemove
load_dotenv()calls(Optional) Define a Pydantic model for typed access
Remove
python-dotenvfrom 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ο
Install Confii:
pip install confiiReplace
from decouple import configwith Confii setupRemove
cast=arguments β type casting is automatic, or use a Pydantic schemaReplace
config("KEY")calls withconfig.keyattribute accessRemove
python-decouplefrom 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:
Pre-resolve interpolation before loading (e.g., with a build step)
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ο
Install Confii:
pip install confiiReplace
OmegaConf.load()withYamlLoaderReplace
OmegaConf.merge()with multiple loaders (later overrides earlier)Replace
OmegaConf.to_container()withconfig.to_dict()Replace
@dataclassstructured configs with PydanticBaseModel+Config[T]Refactor any
${key.ref}interpolation to use env vars or pre-resolveRemove
omegaconffrom 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ο
Install Confii:
pip install confiiReplace
from dynaconf import Settingswithfrom confii import ConfigConvert
Settings(ENV_FOR_DYNACONF="x")toConfig(env="x")Convert
SETTINGS_FILE_FOR_DYNACONF=["a.yaml"]toloaders=[YamlLoader("a.yaml")]Replace
settings.KEYwithconfig.key(Confii is case-sensitive by default)If using Dynaconf Vault: set up
HashiCorpVault+SecretResolverIf using Dynaconf validators: convert to a Pydantic
BaseModelRemove Dynaconf env vars (
*_FOR_DYNACONF) from your environmentRemove
dynaconffrom 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 |
|---|---|
|
|
|
|
|
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ο
Install Confii:
pip install confiiRemove
@hydra.main()decorator from your entry pointReplace
initialize()/compose()withConfig(loaders=[...])Rename
defaults:to_defaults:in your YAML files (or use_include:)Replace
cfg.keywithconfig.key(same attribute access pattern)Convert
@dataclassstructured configs to PydanticBaseModel(optional)Replace
hydra.utils.instantiate()with manual object constructionReplace multirun/sweep with an external tool
Remove
hydra-coreandomegaconffrom 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
.envfiles(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 |
|
|---|---|---|
Pydantic model |
|
|
Typed access |
|
|
Env var loading |
Built-in |
|
File loading |
|
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 |
|
Source tracking |
No |
|
Step-by-Step Checklistο
Install Confii:
pip install confiiChange
BaseSettingstoBaseModel(removeclass Configinner class)Create a
Config[YourModel]instance with your loadersReplace
settings.fieldwithconfig.typed.fieldReplace
env_prefixinclass ConfigwithEnvironmentLoader("PREFIX")orenv_prefix="PREFIX"(Optional) Add file loaders, SSM, secret stores
Remove
pydantic-settingsfrom your dependencies (keeppydantic)
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=valuepairs 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:andhydra:keys, and outputs the remaining config.
What the CLI does NOT doο
It does not rewrite your Python code. You still need to update
importstatements and config access patterns manually.It does not resolve Hydra config group references.
_defaultsentries need manual conversion to_includepaths.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 |
Read from env |
- |
Yes |
Yes (via plugin) |
Yes |
Yes |
Type casting |
- |
Manual |
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 ( |
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 |
- |
|
Config diffing |
- |
- |
- |
- |
- |
- |
Yes |
Variable interpolation |
- |
- |
|
|
|
- |
|
Multirun/sweep |
- |
- |
- |
- |
Yes |
- |
- |
Object instantiation |
- |
- |
- |
- |
|
- |
- |
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:
Check the CLI debug tools (see above)
Check the documentation for API details
File an issue on GitHub with your before/after code and the error you see