1.243 Configuration & Secrets Loading (Python)#
Python configuration loaders compared on typing, layering, secret handling and footprint; secret managers mostly reach the loader as plain environment variables.
Evidence: measured, method not yet published. The numbers here were produced by probes run on a stated machine; the harness is not in the public repository yet, so a reader cannot re-run them.
At a glance#
| Library | How it works | Best for | Latest release |
|---|---|---|---|
| python-dotenv | .env parser; returns a dict or copies into os.environ; no casting | Smallest reader; local development defaults | 1.2.4 · 2026-10-01 |
| python-decouple | Environment, .env and settings.ini with call-site casts | Small apps that want cast= and no dependencies | 3.8 · 2023-03-01 |
| environs | Marshmallow-backed typed accessors over environment variables | Typed environment variables with named validation errors | 15.2.0 · 2026-08-29 |
| pydantic-settings | Pydantic model class filled from env, dotenv, secrets dir, cloud secret managers, CLI | Services with typed, nested settings and cloud secret sources | 2.15.0 · 2026-08-07 |
| Dynaconf | Layered files with per-environment sections, validators, Vault and Redis loaders | File-per-environment configuration and Vault | 3.3.5 · 2026-08-05 |
| Hydra / OmegaConf | Config composition, command-line overrides, multirun sweeps | ML experiments and parameter sweeps | 1.3.7 · 2026-09-14 |
| Standard library (os.environ, tomllib) | Environment mapping and TOML parser, no casting or layering | Three or four variables, or TOML parsing alone | — |
Latest release observed from PyPI in 2026-10.
What the research found
- Process environment beats .env in all four .env-reading loaders probed, and python-dotenv’s override=True is the one switch that reverses it. — A .env file can hold local defaults safely; production precedence needs no extra care.
- load_dotenv() with no path searches upward from the calling file’s directory and loads the first .env it finds, including one in a parent directory. — Pass an explicit path in production code; a stray ancestor .env can be loaded silently.
- Cold import time ranged from 21 ms (python-decouple) to 139 ms (pydantic-settings) and install size from 0.0 to 8.8 MB, on one aarch64 machine. — The cost is irrelevant for services and visible for short-lived processes such as CLIs and cold-start functions.
- Direct secret-manager sources exist in pydantic-settings (AWS, Azure, GCP) and Dynaconf (Vault, Redis); no surveyed loader documents SOPS. — Injecting secrets into the environment (including sops exec-env) works with every loader and avoids a cloud SDK dependency.
- python-decouple shows the stall signature: no release since 2023-03-01 and no merged PRs since April 2026 with 11 open. — Usable as is, but a poor choice for a new project that wants ongoing fixes.
What the research recommends
Default — Choose by validation need and layering: os.environ plus python-dotenv for small apps, pydantic-settings for typed services, Dynaconf for file-per-environment or Vault, Hydra with OmegaConf for experiments. Inject secrets through the environment or files unless rotation volume justifies a direct source.
| Persona | Pick |
|---|---|
| small web app on env vars | python-dotenv with os.environ; environs when casts multiply; pydantic-settings if Pydantic is already a dependency. Pass an explicit .env path. |
| multi environment service | pydantic-settings when settings are modeled in code; Dynaconf when per-environment files or Vault are the organization’s convention. |
| ml experiment config | Hydra with OmegaConf for composition and sweeps; keep tokens in the environment through oc.env. |
| team adopting secret manager | Inject into the environment first (platform, agent or sops exec-env); add pydantic-settings cloud sources or Dynaconf’s Vault loader only where rotation or volume needs them. |
Explainer
Configuration & Secrets Loading (Python): Domain Explainer#
What problem this solves#
A program needs values it cannot hard-code: a database URL, a port, a feature flag, an API token, a learning rate. Those values change between a laptop, a test run and production, and some of them must never appear in source control. Configuration loading is the code that collects those values from wherever they live, converts them from text to the types the program expects, and fails early when one is missing or malformed.
Two things get conflated and are worth separating. Configuration is anything that varies between deployments. Secrets are the subset that grants access to something. A loading library treats both as named values. What differs is where the value may be stored, who may read it, and whether it may appear in a log line.
Key concepts#
Twelve-factor environment variables. The Twelve-Factor App methodology, factor III, says config belongs in the environment, strictly separated from code (source: https://12factor.net/config, accessed 2026-10-05). Environment variables are language-neutral, easy to change per deploy and hard to commit by accident. Their limits are that every value is a string, there is no nesting, the whole set is visible to every child process, and large or multi-line values (certificates) are awkward.
Config files. TOML, YAML, JSON and INI files give nesting and comments and let a team
version non-secret defaults. Python has parsed TOML in the standard library since 3.11
(tomllib, read-only, binary file handle)
(source: https://docs.python.org/3/library/tomllib.html, accessed 2026-10-05).
.env files. A text file of KEY=value lines that a library copies into the process
environment or reads as a dictionary. It is a development convenience that imitates the
production environment. It is not a format with one specification, so quoting,
interpolation and export handling differ between loaders.
Typed validation. Turning "8080" into the integer 8080, "false" into False, and
raising an error that names the bad key. Three styles exist: casting at the call site
(config("PORT", cast=int)), declaring a schema class (pydantic-style), and declaring
validators against a settings object (Dynaconf style).
Layering. Most real configuration is a stack: defaults in code, then a shared file, then an environment-specific file, then environment variables, then command-line flags. The loader’s job is to define the order and the merge rule (replace a nested table whole, or merge it key by key).
Secret backends. A secret manager holds the value, controls who can fetch it, and keeps an audit trail. A loading library’s part is small: authenticate, fetch at startup, and put the value where the typed settings object can see it. Commercial vault products are covered in the 3.xxx series; this survey covers only the Python side of the fetch.
Secret hygiene in the program. Even a correctly fetched secret leaks if it is printed.
Wrapper types such as pydantic’s SecretStr mask the value in repr() and logs and
require an explicit call to reveal it.
The shape of the field#
Four families, with very different weight:
- Environment and
.envreaders: python-dotenv, python-decouple, environs. Thin, fast to adopt, little structure. - Typed settings frameworks: pydantic-settings, Dynaconf. A schema or validator set, several sources, a defined precedence.
- Experiment and composition configs: Hydra with OmegaConf. Built for runs that vary many parameters from the command line, not for services that read secrets.
- The standard library:
os.environ,tomllib,configparser. No dependencies, no casting, no layering.
Common misconceptions#
“A .env file is a secret store.” It is a plaintext file next to the code. It is safe
only when it is untracked, permissioned and absent from production.
"load_dotenv() reads the .env in the current directory." It calls find_dotenv(),
which starts at the directory of the calling file and walks up parent directories
(measured 2026-10-05; python-dotenv 1.2.4; see the harness probe). A stray .env in a
parent directory is loaded silently. In an interactive session, a notebook, a debugger or
a frozen app, it starts from the current working directory instead.
“The .env file wins.” By default load_dotenv() does not override variables already
in the process (measured 2026-10-05; python-dotenv 1.2.4). That is usually what you want
in production and surprising on a laptop.
“Typed settings need a heavy dependency.” The cost spans a range: about 0.1 MB for python-dotenv to about 9 MB for pydantic-settings with its dependencies (measured 2026-10-05; see S2).
How to choose#
- Is this a service, a script or an experiment? Experiments with sweeps point to Hydra/OmegaConf; services point elsewhere.
- Do you need types and a fail-fast error list? That separates the thin readers from the typed frameworks.
- How many deployment environments, and do they share a file? That decides whether you need layering or just environment variables.
- Where do secrets live? A secret manager, an injected file or
sops exec-envall reach the program as environment variables or files, so the loader needs no special support; direct fetching from AWS, Azure, GCP or Vault is where the libraries differ.
Related surveys#
- 1.115 Form Validation Libraries: the same cast-and-report problem for user input.
- 1.060 Cryptographic Libraries: encryption primitives that tools like SOPS, which keeps secrets encrypted inside config files, build on.
- A 3.xxx commercial secret-manager survey was not present in the library on 2026-10-05 (3.011 does not exist); this survey does not compare SaaS vaults.
S1: Rapid Discovery
S1 Rapid Discovery — Approach#
What this category is#
Configuration loading libraries take values from outside the program (environment
variables, .env files, TOML/YAML/JSON files, command-line overrides, secret stores) and
hand them to the program as named, usually typed, settings.
The test for inclusion: the library’s main job is to read configuration into a Python process, and it is on PyPI and maintained as a library rather than as a service.
Category boundary#
In scope
- Environment and
.envreaders: python-dotenv, python-decouple, environs. - Typed settings frameworks: pydantic-settings, Dynaconf.
- Experiment and composition configuration, noted and bounded: Hydra, OmegaConf, Confuse.
- The standard library:
os.environ,tomllib,configparser. - Secret-manager backends as the loaders integrate them: HashiCorp Vault, AWS Secrets Manager, Google Cloud Secret Manager, Azure Key Vault, and SOPS-encrypted files.
Out of scope
- Commercial secret managers as products (vault pricing, rotation features, access policy models). That is the 3.xxx commercial series; no 3.011 survey exists in the library as of 2026-10-05.
- Command-line argument parsers (argparse, click, typer) except where a loader embeds one. A different problem: one invocation, not a deployment.
- Application-specific config such as Django or Flask settings modules, except where a loader ships an extension for them.
- Encryption tools themselves (SOPS, age) beyond how a loader receives their output.
- Runtime feature-flag services. Those change values while the process runs; the libraries here load once at startup (Dynaconf can reload; see S2).
Evaluation dimensions#
- Sources: environment,
.env, files, secret stores, command line. - Typing and validation: cast per call, schema class, or validators; how errors read.
- Layering: defined precedence, per-environment sections, merge rules.
- Secret handling: masking in logs, direct backend fetch, file-based secrets.
- Footprint: dependencies, install size, import time.
- Durability: release cadence, merged versus unanswered patches, governance.
Environment and .env readers#
Three libraries that read the environment and .env files with little structure around
them. GitHub figures were read from api.github.com on 2026-10-05; versions and dates from
PyPI the same day.
| Library | License | Latest release | Stars | Typing | .env | Dependencies |
|---|---|---|---|---|---|---|
| python-dotenv | BSD-3-Clause | 1.2.4, 2026-10-01 | 8.9k | None (strings) | Yes (its job) | None (click for CLI extra) |
| python-decouple | MIT | 3.8, 2023-03-01 | 3.0k | cast= per call | Yes (.env, settings.ini) | None |
| environs | MIT | 15.2.0, 2026-08-29 | 1.4k | Marshmallow fields | Via python-dotenv | marshmallow, python-dotenv |
python-dotenv#
Parses .env files and either returns a dictionary (dotenv_values) or copies the pairs
into os.environ (load_dotenv). It does no type conversion: every value is a string
(measured 2026-10-05; python-dotenv 1.2.4). By default it does not overwrite variables
already in the process; override=True reverses that (measured 2026-10-05). The
no-argument find_dotenv() searches upward from the calling file’s directory, not the
current working directory (measured 2026-10-05); in a REPL, notebook, debugger or frozen
app it falls back to the current working directory. It is also a dependency of
pydantic-settings and environs, which is why it sits under much of the typed stack.
python-decouple#
Reads values from the environment, then .env or settings.ini, and casts at the call
site: config("PORT", cast=int). A missing key with no default raises
UndefinedValueError (measured 2026-10-05; python-decouple 3.8). The last release is 3.8
from March 2023 and the last commit was on 2024-11-28 (source: PyPI and api.github.com,
accessed 2026-10-05); see S4 for the maintenance read.
environs#
A wrapper over marshmallow and python-dotenv with typed accessors: env.int("PORT"),
env.bool(...), env.list(...), env.path(...), env.url(...). Bad values raise
EnvValidationError whose error_messages name the problem (measured 2026-10-05;
environs 15.2.0: Not a valid integer.). Optional extras handle Django-style database
and cache URLs. It validates environment variables only; it does not read TOML or YAML.
Where this family stops#
None of the three defines a schema object, merges files, or fetches from a secret manager. A team that needs a settings object with nested groups, or a layered file stack, moves to the next family.
Experiment and composition configuration#
These tools solve a different problem from the service-oriented libraries: describing a run, with many parameters that vary between runs, and sweeping over them. They are noted here so a reader can see where the category ends, and are not ranked against the others.
| Library | License | Latest release | Stars | Role |
|---|---|---|---|---|
| OmegaConf | BSD-3-Clause | 2.3.1, 2026-06-11 (2.4.0rc1 published 2026-09-28) | 2.4k | Hierarchical config objects, merging, interpolation, resolvers |
| Hydra | MIT | hydra-core 1.3.7, 2026-09-14 | 10.7k | Composition of config groups, command-line overrides, multirun sweeps, built on OmegaConf |
| Confuse | MIT | 2.3.0, 2026-09-22 | 0.4k | Layered YAML config with templated validation and platform config directories |
Versions from PyPI, stars from api.github.com, both accessed 2026-10-05.
OmegaConf#
Builds a config tree from YAML, dictionaries, dotlists or dataclasses and merges trees
with later values winning (measured 2026-10-05; OmegaConf 2.3.1: merging the dotlist
db.host=b over a base replaced only that key). Interpolation can read environment
variables through the oc.env resolver (measured 2026-10-05). It validates against
structured configs (dataclasses), but it is not a schema-first settings framework for
secrets.
Hydra#
Composes a config from groups of YAML files and overrides it from the command line:
python my_app.py db.user=root. --multirun runs the application over a parameter sweep
(source: https://hydra.cc/docs/intro/, accessed 2026-10-05). Its documentation lists
1.3 as stable for Python 3.6 to 3.11, with 1.4 in development for 3.10 to 3.14. It
changes the program’s entry point (a decorator on main) and the working directory per
run, which is the right shape for experiments and the wrong shape for a long-lived
service.
Confuse#
A small library for application configuration with YAML files, command-line merging and a template validation system. It was written for the beets music tagger and has a small community compared with the others. Included to mark the “layered file config” niche.
Boundary#
If the configuration describes a run that will be repeated with variations and logged (a training job, a benchmark), this family fits. If it describes a deployment, the service-oriented libraries fit. Hydra can be combined with environment-variable interpolation for secrets, but it has no secret-manager source.
S1 Recommendation#
Pick by validation need and layering#
| Situation | Start with | Why |
|---|---|---|
A few variables, no .env | os.environ | No dependency; add a from_env() function |
Variables plus a local .env, strings only | python-dotenv | Smallest reader; it is already the base of much of the typed stack |
| Variables with casts, no schema | environs or python-decouple | Typed accessors in one call; environs reports bad values by name |
| A service with typed, nested settings and several sources | pydantic-settings | Schema is the contract; all errors reported together; cloud secret sources built in |
| Many environments in files with overrides, or Vault | Dynaconf | Environment sections and a Vault loader in the box |
| Experiment sweeps | Hydra with OmegaConf | Composition and multirun; not a service tool |
Cautions that apply across the category#
load_dotenv()with no path searches upward from the calling file and does not override existing process variables by default (measured 2026-10-05). Pass an explicit path in production code.- A
.envfile is a development convenience. Production values should arrive through the platform’s environment or an injected secrets file. - python-decouple has had no release since March 2023 (source: PyPI, accessed 2026-10-05); see S4 before adopting it.
The standard library baseline#
Any comparison starts from what Python ships. A surprising amount of configuration loading needs nothing else.
| Tool | What it gives | What it does not |
|---|---|---|
os.environ / os.getenv | Process environment as a string mapping | Casting, defaults beyond .get, required-key errors, .env |
tomllib (3.11+) | Parse TOML to dicts, read-only, binary file handle | Writing, validation, layering, environment overrides |
configparser | INI-style files with sections and interpolation | Types beyond getint/getboolean, nesting |
json | Parse JSON | Comments, layering |
tomllib was added in Python 3.11 and parses only (source:
https://docs.python.org/3/library/tomllib.html, accessed 2026-10-05). The standard library
has no .env reader and no typed settings object; dataclasses plus a hand-written
from_env() is the usual stdlib-only pattern.
Cold import of tomllib costs about 9 ms over bare interpreter start (measured
2026-10-05; Python 3.12, aarch64 WSL2; see S2). That figure is the floor every library
below is measured against.
When the standard library is enough#
A script with three or four variables, each with a default, needs os.environ.get and
int(). The case against stopping there appears at the first required key that is
missing in production: a bare KeyError at the point of use, not a list of everything
that is wrong at startup.
Typed settings frameworks#
Two libraries that define a settings object, several sources and a precedence order.
| Library | License | Latest release | Stars | Schema style | Sources |
|---|---|---|---|---|---|
| pydantic-settings | MIT | 2.15.0, 2026-08-07 | 1.5k | Pydantic model class | env, .env, secrets dir, AWS, Azure, GCP, CLI; TOML/YAML via extras |
| Dynaconf | MIT | 3.3.5, 2026-08-05 | 4.3k | Validator objects on a dynamic object | TOML/YAML/JSON/INI/py, env vars, Vault, Redis, .secrets.* files |
GitHub stars from api.github.com, accessed 2026-10-05; versions and dates from PyPI the same day.
pydantic-settings#
A BaseSettings subclass is a Pydantic model whose fields are filled from the process.
Field types do the casting and validation, and a failed construction raises one Pydantic ValidationError that carries an entry per
failing field (measured 2026-10-05 with one failing field; pydantic-settings 2.15.0). The
documented default priority is init arguments, CLI, environment variables, dotenv files,
secrets files, then external secret managers, and settings_customise_sources() reorders
or replaces them
(source: https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/,
accessed 2026-10-05). SecretStr fields print as ********** and need
get_secret_value() to reveal (measured 2026-10-05). Secret-manager sources exist for
AWS Secrets Manager, AWS Systems Manager Parameter Store, Azure Key Vault and Google
Cloud Secret Manager, installed through extras (aws-secrets-manager, azure-key-vault,
gcp-secret-manager) (source: PyPI metadata for pydantic-settings 2.15.0, accessed
2026-10-05). It is a Pydantic-organization project and requires Pydantic 2.
Dynaconf#
A settings object that loads files and environment variables and exposes values as
attributes. Its distinguishing feature is layered environments inside one file: a
[default] table plus [development], [production] and so on, selected by an env
switch (measured 2026-10-05; Dynaconf 3.3.5: default 1, production 443). An environment
variable with the configured prefix overrides the file value and arrives already typed
(measured 2026-10-05: APP_PORT=7000 gave integer 7000). Validators declare required keys
and conditions. Built-in Vault and Redis loaders exist as extras (vault uses hvac)
(source: https://www.dynaconf.com/secrets/, accessed 2026-10-05). It ships Django and
Flask extensions and a CLI to list and validate. Its bundled extras are the reason
dynaconf[all] is far larger than the base install (see S2).
The difference in one line#
pydantic-settings is a schema first and sources second: the class is the contract. Dynaconf is sources first: files and layers are primary, validation is added on top.
S2: Comprehensive
S2 Comprehensive — Approach#
Four questions separate the libraries, each with a different kind of evidence:
- What does each one do with the same inputs? A probe feeds the same
.envfile, process variables and TOML file through each loader.behavior-probe.md. - What does it cost to depend on? Distribution count, install size and cold import
time.
footprint-and-import-time.md. - What can each one source, cast and layer?
feature-comparison.md, from documentation and package metadata. - How does a secret reach each one? Direct backend sources versus an upstream step
that fills the environment.
secret-backends.md.
Methods note#
Both measurements ran on 2026-10-05 on one machine: Linux 6.6 under WSL2, aarch64, 8
logical CPUs, Python 3.12, uv 0.12.5. The scripts and raw results are in
harness/1-243-configuration-secrets-loading/ at the repository root (measure.py,
results.json, probe.py, probe-output.txt).
- Footprint: each candidate was installed into a fresh
uv venv; the distribution count excludes pip and setuptools; size is the on-disk size ofsite-packages. - Import time: median wall time of 15 fresh interpreter launches of
import <module>, including process start. The median of 15 launches ofpython -c passin the same environment is reported beside it. Timings from a WSL2 laptop are noisy at the millisecond level; differences under about 10 ms should not be read as a ranking. - Probe: a single script (
probe.py) with inlineuvdependency metadata.
No measurement here covers load time of a populated settings object, secret-manager round trips, or behavior on a different architecture. Those need a network peer or a second machine and are not claimed.
Package versions are those uv resolved on 2026-10-05; they are listed in
results.json.
Behavior probe#
The same inputs through each loader. Measured 2026-10-05, Python 3.12, with python-dotenv
1.2.4, python-decouple 3.8, environs 15.2.0, pydantic-settings 2.15.0, Dynaconf 3.3.5 and
OmegaConf 2.3.1. Script and raw output: harness/1-243-configuration-secrets-loading/probe.py
and probe-output.txt.
Inputs: a .env with PORT=8080, DEBUG=false, TOKEN=from_file, BADINT=abc; process
environment PORT=9090, TOKEN=from_process; a settings.toml with a [default] table
(port = 1) and a [production] table (port = 443).
| Observation | Result |
|---|---|
python-dotenv dotenv_values | PORT is the string '8080' |
python-dotenv load_dotenv(path) with TOKEN already set | Process value wins (from_process) |
python-dotenv load_dotenv(path, override=True) | File value wins (from_file) |
python-dotenv find_dotenv() from a file two directories below the .env, cwd elsewhere | Found the parent’s .env by walking up from the file |
python-decouple config("PORT", cast=int) | 9090: environment beat .env |
python-decouple config("DEBUG", cast=bool) with DEBUG=false | False |
| python-decouple missing key without default | Raises UndefinedValueError |
environs env.int("PORT") | 9090 |
environs env.int("BADINT") | EnvValidationError: Not a valid integer. |
| pydantic-settings with one missing required field | ValidationError with one entry, type missing, naming the field |
pydantic-settings fields port, debug, token: SecretStr | 9090, False, SecretStr('**********'); environment beat .env |
Dynaconf, [default] / env="production" | 1 / 443 |
Dynaconf with APP_PORT=7000 set | Integer 7000, overriding the file |
OmegaConf merge of base with dotlist db.host=b, lr=0.2 | Only the named keys changed |
OmegaConf ${oc.env:DB_PASS} | Resolved to the process value |
Findings#
- Precedence agrees across the
.envreaders: process environment beats.envin all four loaders probed with a.envfile (python-dotenv, python-decouple, environs, pydantic-settings); Dynaconf was probed only with an environment variable oversettings.toml, where the environment also wins. Theoverride=Trueflag in python-dotenv is the single place a file can win by configuration. find_dotenv()walks up from the calling file. The probe builds a tree with the.envtwo levels above the script and finds it. The same walk reaches any ancestor, including a home directory, so code that callsload_dotenv()without a path can pick up a file nobody intended it to read.- pydantic-settings aggregates errors by design. Its
ValidationErrorcarries one entry per failing field; the probe triggered one failing field, so multi-field aggregation was not itself exercised. environs reported its single bad variable; decouple raised on the first missing key. - Only pydantic-settings masks secrets by type. The other loaders return plain strings; masking would be the application’s job.
Configuration examples#
The same three settings (a port, a debug flag, a secret token) in each style. These are
sketches of each API’s usual shape; the loading behaviors they rely on were exercised in
behavior-probe.md, while the Dynaconf validator, OmegaConf.from_cli and sops exec-env lines were not run.
Standard library#
import os
PORT = int(os.environ.get("PORT", "8080"))
DEBUG = os.environ.get("DEBUG", "false").lower() in {"1", "true", "yes"}
TOKEN = os.environ["TOKEN"] # KeyError if missing, raised at this linepython-dotenv#
from pathlib import Path
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent / ".env") # explicit path; process values still winenvirons#
from environs import Env
env = Env()
env.read_env(".env")
PORT = env.int("PORT", 8080)
DEBUG = env.bool("DEBUG", False)
TOKEN = env.str("TOKEN")pydantic-settings#
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
port: int = 8080
debug: bool = False
token: SecretStr # printed as SecretStr('**********')
settings = Settings() # one ValidationError if anything is missing or malformedDynaconf#
from dynaconf import Dynaconf, Validator
settings = Dynaconf(
envvar_prefix="APP",
settings_files=["settings.toml"], # [default] plus [production] tables
environments=True,
validators=[Validator("TOKEN", must_exist=True)],
)OmegaConf#
from omegaconf import OmegaConf
cfg = OmegaConf.merge(OmegaConf.load("base.yaml"), OmegaConf.from_cli())
token = OmegaConf.create({"t": "${oc.env:TOKEN}"}).t # resolved from the environmentSecrets from an encrypted file without loader support#
sops exec-env secrets.enc.env 'python app.py' # values appear in the child's environmentFeature comparison#
From documentation, PyPI metadata (accessed 2026-10-05) and the probe in
behavior-probe.md. A dash means not provided by the library.
| stdlib | python-dotenv | python-decouple | environs | pydantic-settings | Dynaconf | OmegaConf / Hydra | |
|---|---|---|---|---|---|---|---|
| Environment variables | os.environ | via load_dotenv | yes | yes | yes | prefixed | oc.env resolver |
.env file | — | yes | yes | yes | yes | yes | — |
| TOML / YAML / JSON files | tomllib, json | — | INI | — | TOML, YAML, JSON (extras for TOML/YAML) | TOML, YAML, JSON, INI, Python | YAML |
| Casting | manual | none | cast= | typed accessors | by field type | inferred from TOML and env syntax | structured configs |
| Schema / validation | — | — | — | per-call | model class | Validator objects | dataclass-backed |
| Aggregated error report | — | — | no (first missing) | per variable | yes | on validators.validate() | on merge/access |
| Per-environment sections | — | — | — | — | via models and env files | yes | config groups (Hydra) |
| Secret masking by type | — | — | — | — | SecretStr | — | — |
| Direct secret-manager source | — | — | — | — | AWS Secrets Manager, AWS SSM, Azure Key Vault, GCP Secret Manager | Vault, Redis | — |
| File-per-secret directory | — | — | — | — | secrets_dir | .secrets.* files | — |
| Command-line source | — | — | — | — | yes (CliSettingsSource) | — | yes (Hydra) |
| Django / Flask integration | — | — | — | URL helpers (Django extras) | — | extensions for both | — |
Sources: https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/ ; https://www.dynaconf.com/ ; https://www.dynaconf.com/secrets/ ; https://hydra.cc/docs/intro/ ; PyPI metadata for each package. Cells for python-decouple and environs that say “no” or “—” reflect the absence of the feature in the documentation and metadata read, not an exhaustive source audit.
Precedence by default#
- pydantic-settings: init arguments, CLI, environment, dotenv, secrets files, then external secret managers; reorderable (source: documentation above).
- Dynaconf: files, then environment variables with the configured prefix on top
(measured 2026-10-05:
APP_PORToverrode the file). - python-dotenv, python-decouple, environs: process environment over
.env(measured 2026-10-05).
Reloading#
Dynaconf can re-read files; the others load once at construction. Services that must pick up a rotated secret without restarting should plan for it explicitly in every case.
Footprint and import time#
Measured 2026-10-05 on Linux 6.6 (WSL2), aarch64, Python 3.12, uv 0.12.5; fresh venv per
row, median of 15 launches. Script: harness/1-243-configuration-secrets-loading/measure.py.
Process start for python -c pass was 10.7 to 13.5 ms across venvs in the same run.
| Candidate (version resolved) | Distributions installed | site-packages MB | Cold import ms (median) |
|---|---|---|---|
stdlib tomllib | 0 | 0.0 | 22.2 |
| python-decouple 3.8 | 1 | 0.0 | 21.1 |
| python-dotenv 1.2.4 | 1 | 0.1 | 34.2 |
| Dynaconf 3.3.5 | 1 | 2.2 | 82.0 |
Dynaconf 3.3.5 with [all] (adds hvac, redis, requests, ruamel.yaml, toml, configobj) | 11 | 8.6 | 84.5 |
| confuse 2.3.0 | 3 | 3.7 | 52.3 |
| OmegaConf 2.3.1 | 3 | 4.8 | 62.4 |
| environs 15.2.0 (marshmallow 4.3.1, python-dotenv) | 3 | 0.6 | 90.9 |
| Hydra hydra-core 1.3.7 | 5 | 6.4 | 129.1 |
| pydantic-settings 2.15.0 (pydantic 2.13.5, pydantic-core 2.46.5, and four small packages) | 7 | 8.8 | 139.5 |
Reading the table#
- Import cost tracks the dependency, not the library. environs is 0.6 MB but imports in about 91 ms because marshmallow does the work. pydantic-settings is the slowest import in the set and brings Pydantic’s compiled core.
- Dynaconf’s base install already costs about 80 ms to import while being one package.
Its
[all]extra adds ten packages and about 6 MB but almost no import time, because the optional loaders import lazily. - python-dotenv versus python-decouple. Both are single-file or near-single-file; the 13 ms gap in this run is within the noise band for this machine.
- The standard library is not free.
tomllibalone takes about 9 ms over interpreter start (22.2 versus a 13.5 ms baseline in the same venv).
What the numbers mean for a decision#
For a long-lived service, 140 ms once at startup is invisible. For a command-line tool that runs often, or a serverless function with a cold start, the difference between 21 and 139 ms is a cost that repeats on every invocation. The install sizes matter most in container images and Lambda-style bundles, where pydantic-settings’s 8.8 MB (about the same as a fully extended Dynaconf) is small next to the rest of a typical image.
Limits#
One machine, one architecture, one Python version. The numbers say nothing about a cold disk cache, about Python 3.13 or 3.14, or about x86. Treat them as relative within this run only.
S2 Recommendation#
Findings#
- Environment beats
.enveverywhere tested, so a file can safely hold local defaults. The one flag that reverses it is python-dotenv’soverride=True(measured 2026-10-05). - Import cost follows the dependency tree. From 21 ms (python-decouple) to 139 ms (pydantic-settings) cold import on one machine, with install sizes from under 0.1 MB to 8.8 MB (measured 2026-10-05; Python 3.12, aarch64). It matters for short-lived processes and not for services.
load_dotenv()without a path can load a.envfrom a parent directory (measured 2026-10-05). Pass the path in production code.- Direct secret-manager sources are concentrated in one library. pydantic-settings
covers AWS, Azure and GCP; Dynaconf covers Vault and Redis. No surveyed loader
documents SOPS;
sops exec-envmakes that irrelevant. - Only pydantic-settings combines a per-field error report with type-level secret masking among the loaders probed.
Stances#
- Default for a new service with typed settings: pydantic-settings.
- Default when settings live in layered files per environment, or Vault is the store: Dynaconf.
- Default for a script or small app: python-dotenv plus
os.environ, or environs when casts matter. - Default for experiments: Hydra with OmegaConf.
- Secrets: prefer injection (environment or files) unless the number of secrets and rotation rate justify a direct source and its SDK.
Secret backends#
Three ways a secret reaches a Python process, and which loaders support each.
| Path | How it works | Loader support |
|---|---|---|
| Direct fetch | The loader authenticates to the backend at startup and reads values | pydantic-settings (AWS Secrets Manager, AWS SSM Parameter Store, Azure Key Vault, GCP Secret Manager); Dynaconf (Vault, Redis) |
| Injected environment | A wrapper, orchestrator or platform sets variables before the process starts | All of them, with no special code |
| Injected file | The platform mounts each secret as a file; the program reads the file | pydantic-settings secrets_dir, Dynaconf .secrets.*; otherwise application code |
Per backend#
HashiCorp Vault. Dynaconf has a built-in loader that uses the hvac client; it is
switched on by environment variables (VAULT_ENABLED_FOR_DYNACONF,
VAULT_URL_FOR_DYNACONF), supports token, AppRole and AWS IAM authentication, and a
KV version setting (source: https://www.dynaconf.com/secrets/, accessed 2026-10-05).
pydantic-settings lists no Vault source in its documentation (read 2026-10-05); a custom
source class fits settings_customise_sources(), or a Vault agent can render values into
the environment.
AWS Secrets Manager and Parameter Store. pydantic-settings provides
AWSSecretsManagerSettingsSource and AWSSystemsManagerSettingsSource; its
aws-secrets-manager extra installs boto3 (source: PyPI metadata, accessed 2026-10-05).
Dynaconf has no AWS source in its documentation (read 2026-10-05).
Google Cloud Secret Manager. pydantic-settings, through the gcp-secret-manager
extra (google-cloud-secret-manager).
Azure Key Vault. pydantic-settings, through the azure-key-vault extra
(azure-identity, azure-keyvault-secrets).
SOPS. SOPS is an editor for encrypted files in YAML, JSON, ENV, INI and binary formats
that encrypts with AWS KMS, GCP KMS, Azure Key Vault, age and PGP; it is a CNCF sandbox
project under MPL-2.0 (source: https://github.com/getsops/sops, accessed 2026-10-05).
None of the loaders surveyed documents a SOPS source. Two integration patterns work with
any of them: sops exec-env <file> <command> runs a process with decrypted values in its
environment, and sops exec-file hands the command a temporary file (FIFO by default)
holding the plaintext (source: https://getsops.io/docs/usage/advanced/, accessed
2026-10-05). Both reach every loader here as ordinary environment or files.
Which path to prefer#
- Direct fetch ties the application to a cloud SDK, adds that SDK to the install (boto3 is large) and makes a network call at startup. It earns its place when the secrets are many, rotate often and the identity of the process is the only credential.
- Injected environment keeps the application cloud-neutral. The cost is that every child process inherits the secrets, and a debug dump of the environment prints them.
- Injected files avoid the environment’s leakiness and suit multi-line values such as certificates. They need a path convention and a file permission story.
Hygiene that no loader provides#
Rotation, access audit and least-privilege policy belong to the secret manager. A loader
that fetches once at startup sees the value that existed at that moment. Masking in
logs exists only where the type provides it (SecretStr); code that copies the value to a
plain string loses the mask.
S3: Need-Driven
S3 Need-Driven — Personas#
Four situations, with what eliminates options in each.
| Persona | Deciding constraint | Eliminated | Remaining |
|---|---|---|---|
| Small web app on environment variables | Few settings, one deployment target, minimal dependencies | Hydra, Dynaconf [all], direct secret sources | os.environ, python-dotenv, environs, python-decouple, pydantic-settings |
| Multi-environment service with typed settings | Several environments, nested settings, startup validation | The string-only readers | pydantic-settings, Dynaconf |
| ML experiment configuration | Many parameters varied per run, sweeps, reproducibility | Service-oriented loaders as the primary tool | Hydra with OmegaConf, Confuse |
| Team adopting a secret manager | Rotation, audit and a path from .env files | Loaders with no sourcing story for the chosen backend | pydantic-settings (cloud managers), Dynaconf (Vault), or injection into any loader |
S3 Recommendation#
| Persona | Pick | Avoid |
|---|---|---|
| Small web app on environment variables | python-dotenv with os.environ; environs for casts; pydantic-settings if Pydantic is already present | Direct secret sources, Hydra |
| Multi-environment service with typed settings | pydantic-settings for code-modeled settings; Dynaconf for file-per-environment or Vault | The string-only readers |
| ML experiment configuration | Hydra with OmegaConf | Service loaders as the main tool |
| Team adopting a secret manager | Inject into the environment first; add a direct source (pydantic-settings for cloud managers, Dynaconf for Vault) only where needed | Expecting the loader to rotate or audit |
Across all four: pass explicit .env paths, keep production sources to the process
environment or mounted files, and mask secrets at the type level where the library allows.
Persona: ML experiment configuration#
Situation#
A researcher or small team runs training and evaluation jobs. Each run has dozens of parameters (model size, learning rate, dataset path, seed). Runs are launched from a shell, swept over parameter grids, and each must record the exact configuration it used.
What it eliminates#
- Environment-variable loaders. Hundreds of nested parameters with sweeps do not fit a flat string namespace.
- Secret-manager sources. The sensitive values here (an API token for a tracking service) are one or two, not the focus.
What remains#
| Option | Role |
|---|---|
| Hydra | Composes config groups, accepts command-line overrides, runs --multirun sweeps (source: https://hydra.cc/docs/intro/, accessed 2026-10-05) |
| OmegaConf | The config object Hydra uses; usable alone for merging YAML, dotlists and dataclasses (measured 2026-10-05) |
| Confuse | A lighter layered YAML option; small community |
| pydantic-settings | Workable for a fixed, typed set; lacks composition groups and sweeps |
Cautions#
- Hydra 1.3 documents Python 3.6 to 3.11 and describes 1.4 as in development for 3.10 to 3.14 (source: https://hydra.cc/docs/intro/, accessed 2026-10-05). OmegaConf 2.4.0rc1 was published on 2026-09-28 (source: PyPI). Check the version pair against the Python in use.
- Hydra changes the working directory of each run by default; code that reads relative paths needs to account for it.
- Secrets can enter through
${oc.env:NAME}interpolation (measured 2026-10-05), so a token stays in the environment and out of the YAML that gets logged with the run. Resolved values still appear in a printed config.
Recommendation#
Hydra with OmegaConf for sweeps and composition; OmegaConf alone when composition is not needed; keep tokens in the environment.
Persona: multi-environment service with typed settings#
Situation#
A team of five to thirty owns a service that runs in development, staging and production, sometimes in several regions. Settings are nested (database pool, cache, feature toggles), some are secrets, and a bad value should stop the process at startup with a readable message, not surface as an exception three requests later.
What it eliminates#
- The string-only readers. Nested, typed settings with a startup error report need a schema or validators.
- Experiment tools. Hydra’s decorator-on-main and per-run working directory are not the shape of a long-lived server.
What remains#
| Option | Layering | Validation | Secret sources |
|---|---|---|---|
| pydantic-settings | Env files per environment chosen by a variable; source order customizable | Pydantic model; errors per field | AWS, Azure, GCP sources; secrets directory |
| Dynaconf | [default] plus per-environment sections in one file; prefixed env override | Validator objects | Vault, Redis, .secrets.* files |
Both were exercised in the probe: Dynaconf selected [production] by an env switch and
let an APP_PORT variable override the file with an integer; pydantic-settings let the
environment beat .env and masked a SecretStr (measured 2026-10-05).
Deciding factors#
- Where the shared defaults live. In a TOML file with environment sections, Dynaconf is the natural fit. In code, as model defaults, pydantic-settings is.
- Type safety downstream. A Pydantic model gives static type information to editors and type checkers; Dynaconf’s settings object is dynamic.
- Footprint. pydantic-settings adds seven distributions and about 8.8 MB; Dynaconf’s base install is one package and 2.2 MB (measured 2026-10-05).
Recommendation#
pydantic-settings for services whose settings are modeled in code; Dynaconf where the organization already keeps per-environment files, or Vault is the source.
Persona: small web app on environment variables#
Situation#
One developer or a pair. A Flask, FastAPI or Django app deployed to a platform that sets
environment variables per service. A .env file for local work. Five to fifteen
settings: database URL, secret key, debug flag, a few third-party API keys.
What it eliminates#
- Frameworks built for layered files or sweeps. The platform already provides the layering: one set of variables per service.
- Direct secret-manager sources. The platform’s variable store is the secret store at this scale, and a cloud SDK in the install is cost without a need.
What remains#
| Option | Fits when |
|---|---|
os.environ plus python-dotenv | Strings are enough; the app converts a couple of values itself |
| environs or python-decouple | Several casts (ints, bools, lists) and a clear error on a bad value |
| pydantic-settings | The app already depends on Pydantic (FastAPI does) and wants masked secret fields |
Cautions#
- Pass an explicit path to
load_dotenv. Without one it walks up from the calling file and may load a.envfrom a parent directory (measured 2026-10-05). - Load
.envonly in development. In production the process environment should be the only source, which is the default precedence in every loader tested. - python-decouple has not had a release since 2023-03-01; environs and python-dotenv were released in 2026 (source: PyPI, accessed 2026-10-05).
Recommendation#
python-dotenv with os.environ for the smallest case; environs when casts multiply;
pydantic-settings when Pydantic is already in the tree.
Persona: team adopting a secret manager#
Situation#
A team has been passing secrets through .env files, chat messages and CI variables. An
incident, an audit requirement or growth prompts a move to a managed secret store (a
cloud provider’s manager, HashiCorp Vault, or encrypted files in the repository). The
question for this survey is only what the Python side has to do. Choosing the product is
the commercial series’ subject.
What it eliminates#
- Any plan that needs the loader to implement rotation or audit. Those belong to the secret manager.
- A rewrite of every service’s configuration code. The cheapest migrations leave the loader unchanged and change what fills the environment.
Three migration paths#
| Path | Python-side change | Fits |
|---|---|---|
| Inject into the environment | None. The platform, an agent or sops exec-env sets variables before launch | Any loader; the least code |
| Mount as files | Read from a secrets directory (secrets_dir in pydantic-settings, .secrets.* in Dynaconf) | Multi-line values; avoiding environment leakage to child processes |
| Fetch directly | Add a source: pydantic-settings for AWS, Azure or GCP; Dynaconf for Vault | Many secrets, frequent rotation, process identity as the credential |
SOPS suits teams that want encrypted secret files versioned alongside code, using KMS or
age keys; sops exec-env decrypts into a child process’s environment (source:
https://getsops.io/docs/usage/advanced/, accessed 2026-10-05).
Cautions#
- Environment injection exposes values to every child process and to anything that dumps the environment.
- Direct fetch adds a cloud SDK to the install and a network call at startup (pydantic-settings installs boto3 through its AWS extra; PyPI metadata, accessed 2026-10-05).
- A fetch at startup sees one version of the secret. Services that must survive rotation need a restart or an explicit reload path.
- Mask secrets in the application:
SecretStrdoes it by type in pydantic-settings (measured 2026-10-05); other loaders return plain strings.
Recommendation#
Start with injection, which changes no code, and add a direct source only where rotation or
volume demands it. Keep .env for local development and out of production images.
S4: Strategic
S4 Strategic — Approach#
Viability signals used, all read on 2026-10-05:
- Release recency and cadence from PyPI.
- Merged versus open pull requests from the GitHub search API, applying the stall test: patches arriving with none merged is a stall; none arriving and none merged is a finished or forgotten project, to be checked against the last commit.
- Governance: which organization owns the repository.
- Python version support declared in package metadata.
A configuration loader is a thin dependency that touches every deployment. Replacing one
is cheap if application code reads settings through one module and expensive if
os.getenv calls are scattered, so the strategic question is mostly how to keep the
loader replaceable.
python-decouple: a stalled but usable dependency#
Sources: PyPI and api.github.com, accessed 2026-10-05.
| Signal | Value |
|---|---|
| Latest release | 3.8, 2023-03-01 |
| Previous releases | 3.7 (2023-01-09), 3.6 (2022-02-02), 3.5 (2021-10-12) |
| Last push | 2024-11-28 |
| Open PRs | 11 |
| PRs merged since 2026-04-01 | 0 |
| License | MIT |
| Stars | 3.0k |
Patches are arriving and none are merged, which is the stall signature. It is a
durability signal, not a judgment of quality: the library is small, has no dependencies and
does what its README says. The 3.8 package declares no requires_python in its metadata
(PyPI, accessed 2026-10-05), so installers cannot warn about an incompatible
interpreter.
The practical risk is a future Python release that breaks it with no maintainer to ship a fix. Mitigation is the replaceability rule in the recommendation: read settings through one module so that moving to environs or pydantic-settings touches one file.
Python version coverage#
Declared requires_python from PyPI metadata, accessed 2026-10-05.
| Library | Version | Declared support |
|---|---|---|
| python-dotenv | 1.2.4 | >=3.10 |
| pydantic-settings | 2.15.0 | >=3.10 |
| environs | 15.2.0 | >=3.10 |
| Dynaconf | 3.3.5 | >=3.10,<3.15 |
| confuse | 2.3.0 | >=3.10 |
| OmegaConf | 2.3.1 | >=3.6 |
| hydra-core | 1.3.7 | not declared |
| python-decouple | 3.8 | not declared |
tomllib | stdlib | 3.11 and newer |
Hydra’s documentation lists 1.3 as stable for Python 3.6 to 3.11 and 1.4 in development for 3.10 to 3.14 (source: https://hydra.cc/docs/intro/, accessed 2026-10-05), while hydra-core 1.3.7 shipped on 2026-09-14. Verify the Hydra and OmegaConf pair on the interpreter in use. Dynaconf’s upper bound at 3.15 means a new Python release needs a Dynaconf update before it installs.
Projects pinned to Python 3.9 or older should check which release of each library last supported their interpreter.
S4 Recommendation#
Keep the loader replaceable#
- Read settings in one module. Application code imports a
settingsobject; only that module knows which library built it. Replacing python-decouple or Dynaconf then touches one file. - Keep the contract in environment variable names. If production sets
DATABASE_URL, any loader can read it. Library-specific prefixes (DYNACONF_-style) tie the deployment to the library. - Keep secrets out of the loader’s API surface. Injection through the environment or files survives a change of loader; a direct-fetch source ties code to a library’s source class.
Switch triggers#
- A library goes a year without a release while patches queue up unmerged (python-decouple matches this now).
- A major Python release lands and the loader’s metadata excludes it.
- Settings grow nested structures that a flat reader cannot express.
Durability ranking for new adoptions#
Healthy and organization-backed: pydantic-settings, Hydra with OmegaConf. Healthy, community-maintained: python-dotenv, environs, Dynaconf. Maintained slowly: Confuse. Stalled: python-decouple. Standard library: no viability risk.
Viability by library#
Sources: PyPI and api.github.com, accessed 2026-10-05. “Merged” counts pull requests merged since 2026-04-01 (OmegaConf and Confuse queried in a second pass the same day). Counts include automated dependency PRs, which inflate Hydra and similar projects.
| Library | Latest release | Open PRs | Merged since 2026-04-01 | Last push | Read |
|---|---|---|---|---|---|
| python-dotenv | 1.2.4, 2026-10-01 | 67 | 18 | 2026-10-01 | Healthy; a backlog of open PRs |
| pydantic-settings | 2.15.0, 2026-08-07 | 11 | 110 | 2026-10-02 | Healthy; Pydantic organization |
| Dynaconf | 3.3.5, 2026-08-05 | 22 | 45 | 2026-10-01 | Healthy; Python up to 3.14 declared |
| environs | 15.2.0, 2026-08-29 | 2 | 20 | 2026-09-08 | Healthy; small open backlog |
| python-decouple | 3.8, 2023-03-01 | 11 | 0 | 2024-11-28 | Stalled: patches arriving, none merged, no release in 3.5 years |
| Hydra | 1.3.7, 2026-09-14 | 3 | 177 (includes automated PRs, unverified split) | 2026-10-05 | Active; 1.4 documented as in development |
| OmegaConf | 2.3.1, 2026-06-11; 2.4.0rc1, 2026-09-28 | 0 | 96 | 2026-10-05 | Active; release candidate out |
| Confuse | 2.3.0, 2026-09-22 | 17 | 4 | 2026-09-22 | Slow but maintained |
python-decouple#
The stall signature applies: 11 open pull requests, no merged pull request since January 2024, last
commit November 2024, last release March 2023 (sources above). It is MIT-licensed,
dependency-free and small, so the practical risk is low today and rises with Python
version changes. It declares no requires_python in its metadata (PyPI, accessed
2026-10-05). Use it for what it does; do not rely on fixes.
Python version coverage#
pydantic-settings, python-dotenv, environs and confuse declare Python 3.10 or newer
(PyPI metadata). Dynaconf declares >=3.10,<3.15. Hydra’s documentation lists 1.3 as
stable for Python up to 3.11 and 1.4 in development for up to 3.14, while hydra-core
1.3.7 shipped in September 2026 (sources above); verify the pair on your interpreter.
Standard library#
tomllib has been in the standard library since 3.11 and carries Python’s own support
policy. A project that needs only TOML parsing and environment access has no
third-party viability question at all.