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#

LibraryHow it worksBest forLatest release
python-dotenv.env parser; returns a dict or copies into os.environ; no castingSmallest reader; local development defaults1.2.4 · 2026-10-01
python-decoupleEnvironment, .env and settings.ini with call-site castsSmall apps that want cast= and no dependencies3.8 · 2023-03-01
environsMarshmallow-backed typed accessors over environment variablesTyped environment variables with named validation errors15.2.0 · 2026-08-29
pydantic-settingsPydantic model class filled from env, dotenv, secrets dir, cloud secret managers, CLIServices with typed, nested settings and cloud secret sources2.15.0 · 2026-08-07
DynaconfLayered files with per-environment sections, validators, Vault and Redis loadersFile-per-environment configuration and Vault3.3.5 · 2026-08-05
Hydra / OmegaConfConfig composition, command-line overrides, multirun sweepsML experiments and parameter sweeps1.3.7 · 2026-09-14
Standard library (os.environ, tomllib)Environment mapping and TOML parser, no casting or layeringThree 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.

PersonaPick
small web app on env varspython-dotenv with os.environ; environs when casts multiply; pydantic-settings if Pydantic is already a dependency. Pass an explicit .env path.
multi environment servicepydantic-settings when settings are modeled in code; Dynaconf when per-environment files or Vault are the organization’s convention.
ml experiment configHydra with OmegaConf for composition and sweeps; keep tokens in the environment through oc.env.
team adopting secret managerInject 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:

  1. Environment and .env readers: python-dotenv, python-decouple, environs. Thin, fast to adopt, little structure.
  2. Typed settings frameworks: pydantic-settings, Dynaconf. A schema or validator set, several sources, a defined precedence.
  3. Experiment and composition configs: Hydra with OmegaConf. Built for runs that vary many parameters from the command line, not for services that read secrets.
  4. 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#

  1. Is this a service, a script or an experiment? Experiments with sweeps point to Hydra/OmegaConf; services point elsewhere.
  2. Do you need types and a fail-fast error list? That separates the thin readers from the typed frameworks.
  3. How many deployment environments, and do they share a file? That decides whether you need layering or just environment variables.
  4. Where do secrets live? A secret manager, an injected file or sops exec-env all 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.
  • 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 .env readers: 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#

  1. Sources: environment, .env, files, secret stores, command line.
  2. Typing and validation: cast per call, schema class, or validators; how errors read.
  3. Layering: defined precedence, per-environment sections, merge rules.
  4. Secret handling: masking in logs, direct backend fetch, file-based secrets.
  5. Footprint: dependencies, install size, import time.
  6. 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.

LibraryLicenseLatest releaseStarsTyping.envDependencies
python-dotenvBSD-3-Clause1.2.4, 2026-10-018.9kNone (strings)Yes (its job)None (click for CLI extra)
python-decoupleMIT3.8, 2023-03-013.0kcast= per callYes (.env, settings.ini)None
environsMIT15.2.0, 2026-08-291.4kMarshmallow fieldsVia python-dotenvmarshmallow, 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.

LibraryLicenseLatest releaseStarsRole
OmegaConfBSD-3-Clause2.3.1, 2026-06-11 (2.4.0rc1 published 2026-09-28)2.4kHierarchical config objects, merging, interpolation, resolvers
HydraMIThydra-core 1.3.7, 2026-09-1410.7kComposition of config groups, command-line overrides, multirun sweeps, built on OmegaConf
ConfuseMIT2.3.0, 2026-09-220.4kLayered 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#

SituationStart withWhy
A few variables, no .envos.environNo dependency; add a from_env() function
Variables plus a local .env, strings onlypython-dotenvSmallest reader; it is already the base of much of the typed stack
Variables with casts, no schemaenvirons or python-decoupleTyped accessors in one call; environs reports bad values by name
A service with typed, nested settings and several sourcespydantic-settingsSchema is the contract; all errors reported together; cloud secret sources built in
Many environments in files with overrides, or VaultDynaconfEnvironment sections and a Vault loader in the box
Experiment sweepsHydra with OmegaConfComposition 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 .env file 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.

ToolWhat it givesWhat it does not
os.environ / os.getenvProcess environment as a string mappingCasting, defaults beyond .get, required-key errors, .env
tomllib (3.11+)Parse TOML to dicts, read-only, binary file handleWriting, validation, layering, environment overrides
configparserINI-style files with sections and interpolationTypes beyond getint/getboolean, nesting
jsonParse JSONComments, 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.

LibraryLicenseLatest releaseStarsSchema styleSources
pydantic-settingsMIT2.15.0, 2026-08-071.5kPydantic model classenv, .env, secrets dir, AWS, Azure, GCP, CLI; TOML/YAML via extras
DynaconfMIT3.3.5, 2026-08-054.3kValidator objects on a dynamic objectTOML/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:

  1. What does each one do with the same inputs? A probe feeds the same .env file, process variables and TOML file through each loader. behavior-probe.md.
  2. What does it cost to depend on? Distribution count, install size and cold import time. footprint-and-import-time.md.
  3. What can each one source, cast and layer? feature-comparison.md, from documentation and package metadata.
  4. 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 of site-packages.
  • Import time: median wall time of 15 fresh interpreter launches of import <module>, including process start. The median of 15 launches of python -c pass in 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 inline uv dependency 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).

ObservationResult
python-dotenv dotenv_valuesPORT is the string '8080'
python-dotenv load_dotenv(path) with TOKEN already setProcess 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 elsewhereFound 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=falseFalse
python-decouple missing key without defaultRaises UndefinedValueError
environs env.int("PORT")9090
environs env.int("BADINT")EnvValidationError: Not a valid integer.
pydantic-settings with one missing required fieldValidationError with one entry, type missing, naming the field
pydantic-settings fields port, debug, token: SecretStr9090, False, SecretStr('**********'); environment beat .env
Dynaconf, [default] / env="production"1 / 443
Dynaconf with APP_PORT=7000 setInteger 7000, overriding the file
OmegaConf merge of base with dotlist db.host=b, lr=0.2Only the named keys changed
OmegaConf ${oc.env:DB_PASS}Resolved to the process value

Findings#

  1. Precedence agrees across the .env readers: process environment beats .env in all four loaders probed with a .env file (python-dotenv, python-decouple, environs, pydantic-settings); Dynaconf was probed only with an environment variable over settings.toml, where the environment also wins. The override=True flag in python-dotenv is the single place a file can win by configuration.
  2. find_dotenv() walks up from the calling file. The probe builds a tree with the .env two levels above the script and finds it. The same walk reaches any ancestor, including a home directory, so code that calls load_dotenv() without a path can pick up a file nobody intended it to read.
  3. pydantic-settings aggregates errors by design. Its ValidationError carries 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.
  4. 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 line

python-dotenv#

from pathlib import Path
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent / ".env")   # explicit path; process values still win

environs#

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 malformed

Dynaconf#

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 environment

Secrets from an encrypted file without loader support#

sops exec-env secrets.enc.env 'python app.py'   # values appear in the child's environment

Feature comparison#

From documentation, PyPI metadata (accessed 2026-10-05) and the probe in behavior-probe.md. A dash means not provided by the library.

stdlibpython-dotenvpython-decoupleenvironspydantic-settingsDynaconfOmegaConf / Hydra
Environment variablesos.environvia load_dotenvyesyesyesprefixedoc.env resolver
.env file—yesyesyesyesyes—
TOML / YAML / JSON filestomllib, json—INI—TOML, YAML, JSON (extras for TOML/YAML)TOML, YAML, JSON, INI, PythonYAML
Castingmanualnonecast=typed accessorsby field typeinferred from TOML and env syntaxstructured configs
Schema / validation———per-callmodel classValidator objectsdataclass-backed
Aggregated error report——no (first missing)per variableyeson validators.validate()on merge/access
Per-environment sections————via models and env filesyesconfig groups (Hydra)
Secret masking by type————SecretStr——
Direct secret-manager source————AWS Secrets Manager, AWS SSM, Azure Key Vault, GCP Secret ManagerVault, 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_PORT overrode 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 installedsite-packages MBCold import ms (median)
stdlib tomllib00.022.2
python-decouple 3.810.021.1
python-dotenv 1.2.410.134.2
Dynaconf 3.3.512.282.0
Dynaconf 3.3.5 with [all] (adds hvac, redis, requests, ruamel.yaml, toml, configobj)118.684.5
confuse 2.3.033.752.3
OmegaConf 2.3.134.862.4
environs 15.2.0 (marshmallow 4.3.1, python-dotenv)30.690.9
Hydra hydra-core 1.3.756.4129.1
pydantic-settings 2.15.0 (pydantic 2.13.5, pydantic-core 2.46.5, and four small packages)78.8139.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. tomllib alone 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#

  1. Environment beats .env everywhere tested, so a file can safely hold local defaults. The one flag that reverses it is python-dotenv’s override=True (measured 2026-10-05).
  2. 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.
  3. load_dotenv() without a path can load a .env from a parent directory (measured 2026-10-05). Pass the path in production code.
  4. 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-env makes that irrelevant.
  5. 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.

PathHow it worksLoader support
Direct fetchThe loader authenticates to the backend at startup and reads valuespydantic-settings (AWS Secrets Manager, AWS SSM Parameter Store, Azure Key Vault, GCP Secret Manager); Dynaconf (Vault, Redis)
Injected environmentA wrapper, orchestrator or platform sets variables before the process startsAll of them, with no special code
Injected fileThe platform mounts each secret as a file; the program reads the filepydantic-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.

PersonaDeciding constraintEliminatedRemaining
Small web app on environment variablesFew settings, one deployment target, minimal dependenciesHydra, Dynaconf [all], direct secret sourcesos.environ, python-dotenv, environs, python-decouple, pydantic-settings
Multi-environment service with typed settingsSeveral environments, nested settings, startup validationThe string-only readerspydantic-settings, Dynaconf
ML experiment configurationMany parameters varied per run, sweeps, reproducibilityService-oriented loaders as the primary toolHydra with OmegaConf, Confuse
Team adopting a secret managerRotation, audit and a path from .env filesLoaders with no sourcing story for the chosen backendpydantic-settings (cloud managers), Dynaconf (Vault), or injection into any loader

S3 Recommendation#

PersonaPickAvoid
Small web app on environment variablespython-dotenv with os.environ; environs for casts; pydantic-settings if Pydantic is already presentDirect secret sources, Hydra
Multi-environment service with typed settingspydantic-settings for code-modeled settings; Dynaconf for file-per-environment or VaultThe string-only readers
ML experiment configurationHydra with OmegaConfService loaders as the main tool
Team adopting a secret managerInject into the environment first; add a direct source (pydantic-settings for cloud managers, Dynaconf for Vault) only where neededExpecting 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#

OptionRole
HydraComposes config groups, accepts command-line overrides, runs --multirun sweeps (source: https://hydra.cc/docs/intro/, accessed 2026-10-05)
OmegaConfThe config object Hydra uses; usable alone for merging YAML, dotlists and dataclasses (measured 2026-10-05)
ConfuseA lighter layered YAML option; small community
pydantic-settingsWorkable 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#

OptionLayeringValidationSecret sources
pydantic-settingsEnv files per environment chosen by a variable; source order customizablePydantic model; errors per fieldAWS, Azure, GCP sources; secrets directory
Dynaconf[default] plus per-environment sections in one file; prefixed env overrideValidator objectsVault, 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#

OptionFits when
os.environ plus python-dotenvStrings are enough; the app converts a couple of values itself
environs or python-decoupleSeveral casts (ints, bools, lists) and a clear error on a bad value
pydantic-settingsThe 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 .env from a parent directory (measured 2026-10-05).
  • Load .env only 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#

PathPython-side changeFits
Inject into the environmentNone. The platform, an agent or sops exec-env sets variables before launchAny loader; the least code
Mount as filesRead from a secrets directory (secrets_dir in pydantic-settings, .secrets.* in Dynaconf)Multi-line values; avoiding environment leakage to child processes
Fetch directlyAdd a source: pydantic-settings for AWS, Azure or GCP; Dynaconf for VaultMany 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: SecretStr does 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.

SignalValue
Latest release3.8, 2023-03-01
Previous releases3.7 (2023-01-09), 3.6 (2022-02-02), 3.5 (2021-10-12)
Last push2024-11-28
Open PRs11
PRs merged since 2026-04-010
LicenseMIT
Stars3.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.

LibraryVersionDeclared support
python-dotenv1.2.4>=3.10
pydantic-settings2.15.0>=3.10
environs15.2.0>=3.10
Dynaconf3.3.5>=3.10,<3.15
confuse2.3.0>=3.10
OmegaConf2.3.1>=3.6
hydra-core1.3.7not declared
python-decouple3.8not declared
tomllibstdlib3.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 settings object; 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.

LibraryLatest releaseOpen PRsMerged since 2026-04-01Last pushRead
python-dotenv1.2.4, 2026-10-0167182026-10-01Healthy; a backlog of open PRs
pydantic-settings2.15.0, 2026-08-07111102026-10-02Healthy; Pydantic organization
Dynaconf3.3.5, 2026-08-0522452026-10-01Healthy; Python up to 3.14 declared
environs15.2.0, 2026-08-292202026-09-08Healthy; small open backlog
python-decouple3.8, 2023-03-011102024-11-28Stalled: patches arriving, none merged, no release in 3.5 years
Hydra1.3.7, 2026-09-143177 (includes automated PRs, unverified split)2026-10-05Active; 1.4 documented as in development
OmegaConf2.3.1, 2026-06-11; 2.4.0rc1, 2026-09-280962026-10-05Active; release candidate out
Confuse2.3.0, 2026-09-221742026-09-22Slow 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.

Published: 2026-10-05 Updated: 2026-10-05