environs
Type-safe environment variable parsing for Python, built on marshmallow.
Repository Health
Technical Analysis
environs is a Python library for parsing and validating environment variables, letting teams keep configuration separate from code in line with the Twelve-Factor App methodology. Instead of manually reading os.environ and casting strings by hand, environs exposes a single Env object with typed accessor methods — env.str(), env.int(), env.bool(), env.list(), env.dict(), env.datetime(), env.url(), and more — each of which validates and coerces the raw string value, raising a clear EnvValidationError when a required variable is missing or malformed.
Under the hood, every accessor is a thin wrapper around a marshmallow field, so environs inherits marshmallow’s validation machinery for free: built-in validators like OneOf and Email, custom validator functions, and deferred/eager validation modes. read_env() loads .env files into an isolated dictionary rather than mutating os.environ, and supports prefixed variable groups, ${VAR}-style variable expansion, and Docker-style _FILE secret references via the FileAwareEnv subclass. First-class helpers for Flask and Django settings modules make it a common substitute for hand-rolled os.getenv() calls in web app configuration.
What You Get
- A typed
Envobject with dedicated methods for str, bool, int, float, decimal, list, dict, json, datetime, date, time, timedelta, path, log_level, uuid, url, and enum values .envfile loading viaread_env()that populates an isolated dictionary instead of mutatingos.environ, with directory-recursion and multi-file override support- Built-in validation powered by marshmallow’s
validatemodule (OneOf,Email,Length,Range, custom callables) with both eager (raise-on-first-error) and deferred (seal()-collected) modes - Prefixed and nested variable groups via
Env(prefix=...)or theenv.prefixed()context manager, useful for namespacing config likeMYAPP_DB_HOST ${VAR:-default}-style variable expansion for referencing one environment variable’s value from anotherFileAwareEnvfor Docker/Kubernetes-style secrets, automatically reading a value from the file path in a_FILE-suffixed variable- Custom parser registration via
env.parser_for()/add_parser()for domain-specific value types - Optional Django integration (
dj_db_url,dj_email_url,dj_cache_url) for parsing database, email, and cache connection URLs directly into config dicts
Common Use Cases
- Twelve-Factor app configuration - loading all runtime config (ports, feature flags, secrets, database URLs) from environment variables instead of hardcoded settings files
- Flask/Django settings modules - replacing manual
os.getenv()+int()/bool()casting insettings.pywith typed, validated accessors that fail fast on misconfiguration - Local development with .env files - using
env.read_env()so developers can keep secrets and local overrides in a gitignored.envfile without touching shipped code - Container/Kubernetes secret injection - using
FileAwareEnvto transparently read secrets mounted as files (the Docker secrets convention) without changing application code - Strict startup validation - using
eager=Falseplusenv.seal()to collect every missing/invalid environment variable at once and fail deployment with one comprehensive error instead of one variable at a time
Under The Hood
Architecture
The Env class holds a small set of typed accessor methods (int, bool, str, list, dict, url, etc.) generated at class-definition time by the _field2method/_func2method factory functions, each closure wrapping a marshmallow Field construction. Every accessor calls _get_from_environ, which resolves prefix scoping and ${VAR:-default} expansion (via the _EXPANDED_VAR_PATTERN regex and _expand_vars), then runs field.deserialize() to cast and validate, storing results in _values/_fields and, in deferred mode, accumulating messages in an _errors defaultdict. FileAwareEnv subclasses Env and overrides only _get_value() to check a <KEY>_FILE indirection before falling back to the parent lookup — a clean single-method extension point. Custom parsers registered via parser_for()/add_parser() land in __custom_parsers__ and are resolved dynamically through __getattr__. Because every typed accessor is generated from the same two factory functions, a change to _field2method or _func2method has the widest blast radius in the codebase.
Tech Stack
Built with the flit_core build backend, targeting Python 3.10+, with core runtime dependencies on python-dotenv (for .env parsing via dotenv_values) and marshmallow>=4.0.0 (the validation/casting engine behind every accessor). Optional Django integrations (dj-database-url, dj-email-url, django-cache-url) are lazily imported only when their corresponding dj_db_url()/dj_email_url()/dj_cache_url() methods are called, keeping the default install lightweight. Tooling is uv-managed (uv.lock committed) with tox/tox-uv driving a multi-Python test matrix, ruff for linting/formatting, and mypy for type checking, all wired into GitHub Actions (build-release.yml), which runs the tox matrix, builds and twine-validates the wheel, and gates PyPI publication on a passing lint job for tagged releases.
Code Quality
A single 1,363-line pytest suite (tests/test_environs.py) covers every typed accessor, prefix scoping, variable expansion, .env loading (including directory recursion and override semantics), deferred validation via seal(), FileAwareEnv, and custom parser registration — a substantial ratio against the roughly 1,076 lines of library source. A dedicated tests/mypy_test_cases directory exercises the type stubs directly. ruff is configured with select = ["ALL"] and a short, justified ignore list, so nearly every lint category is active; the codebase is fully annotated with from __future__ import annotations and ships a py.typed marker. Errors are explicit and typed rather than generic: a small dedicated hierarchy (EnvError, EnvNotSetError, EnvSealedError, EnvValidationError, ParserConflictError) backs both the eager-raise and deferred-collect error paths.
What Makes It Unique
Rather than reimplementing type-casting and validation, environs delegates every accessor to marshmallow’s Field/Schema machinery, inheriting validator reuse, consistent error formatting, and a dump() method that serializes parsed values back through the same schema. The FileAwareEnv/_FILE-suffix convention for Docker- and Kubernetes-style secret files, plus the ${VAR:-default} expansion syntax for cross-referencing variables, are its most distinctive features relative to plain python-dotenv or hand-rolled os.environ parsing — both address operational patterns that generic env-parsing tools typically skip. The novelty is less a new parsing algorithm than an efficient, well-tested reuse of an established validation ecosystem.