environs reads .env files into its own object, and never touches os.environ
simplified environment variable parsing
At a glance
- What is it?
- A Python library for parsing environment variables with type casting, validation, prefixes and variable expansion, built on marshmallow for the schemas and python-dotenv for the files. The decision to keep .env values out of the process environment is the one that changes how your code behaves.
- Who is it for?
- Adopt environs if you want typed configuration with validation in a Flask, Django or plain Python service, and you want .env files to be readable without them leaking into the process environment. Do not adopt it if you need library code to read os.environ directly, because values loaded from a .env file are only visible through the Env instance, which is a deliberate constraint rather than an accident.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The .env file does not become part of os.environ
environs is a Python library for parsing environment variables, and it allows you to store configuration separate from your code as per the Twelve-Factor App methodology.
The feature list is five items, and the second one is the design decision that matters most. Type-casting. Read `.env` files without mutating `os.environ`. Validation. Define custom parser behavior. And be framework-agnostic, but integrate well with Flask and Django.
That second item is the one to understand before you adopt it. The `read_env` method loads values into the `Env` instance, and the README notes explicitly that it loads values into the `Env` instance without modifying `os.environ`.
The practical consequence is that a library you depend on, which reads the environment directly, will not see the value from your `.env` file. Only code that goes through the `Env` instance can read it. For an application where you control all the configuration access, that is a feature, because it makes the source of every setting explicit. For an application with third party libraries that read the environment themselves, it is a constraint you have to work around.
The library is MIT licensed, the version is 15.2.0 in the metadata, and it requires Python 3.10 or newer with classifiers through 3.14. The dependencies are three: `python-dotenv` for reading the files, `marshmallow` at 4.0.0 or newer for the validation, and `typing-extensions` for Python versions below 3.11.
The marshmallow dependency is the interesting one, because it explains the design of the whole library. Validation is not reinvented, it is delegated to marshmallow's schema system, which means the validators you already know work here, and which is why the built-in validators are described as provided by marshmallow.
Casting covers the types a configuration actually needs
The type-casting methods are the API you will use every day, and the list is a good guide to what the library considers a configuration value.
`env.str`, `env.bool`, `env.int`, `env.float` and `env.decimal` are the obvious ones. `env.list` accepts optional `subcast` and `delimiter` keyword arguments, and `env.dict` accepts `subcast_keys`, `subcast_values`, `delimiter` and `key_value_delimiter`.
There is `env.json`, and a family of date and time ones: `env.datetime`, `env.date`, `env.time` and `env.timedelta`. The timedelta one assumes the value is an integer in seconds, or an ordered duration string like `7h7s` or `7w 7d 7h 7m 7s 7ms 7us`.
The URL handling is the one with a sharp edge, and the README is explicit about it. `env.url` returns a `urllib.parse.ParseResult` and therefore expects a `ParseResult` for its default, which means a default of a plain string will not work.
from urllib.parse import urlparse
from environs import env
MY_API_URL = env.url(
"MY_API_URL",
default=urlparse("http://api.example.com"),
)If you want a string back, the README says to use `env.str` with `validate.URL` instead.
The rest are `env.uuid`, `env.log_level`, `env.path` which casts to a `pathlib.Path`, and `env.enum` which casts to any given enum type specified in an `enum` keyword argument, with a `by_value=True` option to parse and validate by the enum's values rather than its names.
The usage example shows the whole range in one place, including the list and dict parsing.
gh_repos = env.list("GITHUB_REPOS") # => ['webargs', 'konch', 'ped']
coords = env.list("COORDINATES", subcast=float) # => [23.3, 50.0]
locations = env.dict(
"LOCATIONS", subcast_values=int, delimiter=" ", key_value_delimiter=":"
) # => {'x': 234, 'y': 123}The subcast argument is what makes the list and dict methods useful rather than a string splitter, because it casts each element as it is parsed.
Prefixes and variable expansion are the two features you will use every day
Two features handle the cases where a raw environment variable name is not enough.
The first is prefixes. You pass `prefix` to the constructor if all your environment variables have the same prefix.
from environs import Env
env = Env(prefix="MYAPP_")
host = env("HOST", "localhost") # => 'lolcathost'
port = env.int("PORT", 5000) # => 3000There is also a `prefixed` context manager, which is the better form when the prefix is local to a block rather than a property of the whole application.
with env.prefixed("MYAPP_"):
host = env("HOST", "localhost")Nested prefixes are supported too, with `env.prefixed("DB_")` inside `env.prefixed("MYAPP_")` for a `MYAPP_DB_HOST` style variable. The context manager form is worth preferring in a library or a module, because the prefix does not leak into the rest of the process.
The second feature is variable expansion, which is off by default and enabled with `expand_vars=True`.
env = Env(expand_vars=True)
connection_url = env("CONNECTION_URL") # =>'https://sloria:secret@localhost'
year = env.int("YEAR") # =>2020The syntax is shell-like, with `${VAR}` and a default form `${VAR:-default}`. The example uses a URL with a default for the user and the host, with the password taken from the environment.
Two details are worth noting. The expansion is done at read time, so the value of the password has to exist by then, and the default syntax with the colon is what distinguishes a default from an empty value, which is the behaviour you want when a variable is set but empty.
Validation is marshmallow, and deferred validation lets you decide when
The validation section is built on marshmallow, and the built-in validators are described as provided by marshmallow, so `validate.OneOf` and the rest work as you already know them.
The imports for that are the environment helpers plus the validate module and the ValidationError type.
The important part is the error type, which is `ValidationError` from the library rather than marshmallow's own, and the fact that the message is constructed with the variable name so a failure points at the setting rather than at a value.
Deferred validation is the feature that changes how a framework integration behaves, and the examples directory names it: `deferred_validation_example.py`. With deferred validation, the value is not parsed and validated when it is read but later, which matters for Django where settings are imported at startup and a missing variable would otherwise crash the import before the app can show a helpful error page.
The other examples in the repository show the feature surface. There is `simple_example.py`, `validation_example.py`, `prefix_example.py`, `marshmallow_example.py`, `plugin_example.py` for custom parser behavior, `fileaware_example.py` for reading a specific file, and `django_example.py`.
The Docker style secret file support is worth a mention because it is a deployment pattern that is otherwise awkward. The library can read a secret from a file rather than a variable, which is how Docker and Kubernetes deliver secrets.
The custom parser behavior feature is the extension point. It is what lets you add a type the library does not have, and the `plugin_example.py` in the examples is the reference implementation.
Flask and Django integrations, and why the library exists at all
The framework integrations are the reason the library is popular, and the mechanics differ between the two.
For Flask, the integration works because Flask's configuration is already an object of parsed values, so environs can populate it directly. For Django, the settings module is the integration point, and the Django extra pulls in `dj-database-url`, `dj-email-url` and `django-cache-url`, which are the URL parsers for database, email and cache backends that Django settings conventionally express as URLs.
That is a small but telling dependency list. The Django extra exists because the common Django settings pattern is a set of URL variables, and environs can parse and validate them into the typed values Django wants.
The Why section answers the three questions a reader has, and the answers are the clearest statement of the library's position.
On why environment variables at all: it is the Twelve-Factor methodology, storing configuration in the environment rather than in the code, so the same artifact runs in every environment.
On why not `os.environ`: the raw values are strings, and you end up with a scattering of conversions that fail at the point of use, with no defaults declared in one place and no validation. environs gives you one declarative place where a variable is named, cast, defaulted and validated.
On why another library: there are existing options, and the argument is that this one is deliberately small, integrates with the frameworks, and does the typed parsing without requiring a schema for every project.
The repository layout is conventional and the tooling is current. There is a `src/` directory, `tests/`, `examples/`, a `pyproject.toml` with central dependency groups, a `tox.ini`, a `uv.lock`, a `renovate.json` and a `.pre-commit-config.yaml`. The build backend is flit_core, the linter is Ruff configured with the `ALL` rule set and a line length of 90, and the CI workflow is a build and release workflow. The last push to the repository was on 2026-10-09 and it is not archived, so the library is actively maintained.
Editorial conclusion
Adopt environs if you want typed configuration with validation in a Flask, Django or plain Python service, and you want .env files to be readable without them leaking into the process environment. Do not adopt it if you need library code to read os.environ directly, because values loaded from a .env file are only visible through the Env instance, which is a deliberate constraint rather than an accident. Verify first that the types you need are covered by the casting methods, that your validation errors are surfaced the way you want, and that the marshmallow version in your environment satisfies the library's requirement.
Frequently asked questions
Does environs modify os.environ when it reads a .env file?
No. The README states that read_env loads values into the Env instance without modifying os.environ, which means code that reads the environment directly will not see the values from a .env file. Only code going through the Env instance can read them.
Which types can environs cast?
str, bool, int, float, decimal, list, dict, json, datetime, date, time, timedelta, url, uuid, log_level, path and enum. Lists and dicts accept subcast, delimiter and key_value_delimiter arguments, and env.url returns a urllib ParseResult and expects one as its default.
How do I use prefixed environment variables with environs?
Pass prefix to the Env constructor for an application-wide prefix, or use the prefixed context manager for a scoped one. Nested prefixes are supported, so env.prefixed("DB_") inside env.prefixed("MYAPP_") resolves a MYAPP_DB_HOST style variable.
How does validation work in environs?
Validation is delegated to marshmallow, so the built-in validators work as you already know them, and failures raise the library's ValidationError naming the variable. Deferred validation postpones parsing and validation so a missing setting does not crash at import time, which matters for Django.
What are the dependencies of environs?
python-dotenv, marshmallow at 4.0.0 or newer, and typing-extensions for Python below 3.11. The Django extra adds dj-database-url, dj-email-url and django-cache-url. The library requires Python 3.10 or newer and is MIT licensed.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sloria-environs)Was this useful?