# python-dotenv: loading .env files into Python without touching production config

> python-dotenv reads key-value pairs from a .env file and sets them as environment variables, which makes it a development convenience rather than a configuration system. The library is current (last push 2026-08-23), BSD-3-Clause, and needs Python 3.10 or newer.

**theskumar/python-dotenv** — Reads key-value pairs from a .env file and can set them as environment variables. It helps in developing applications following the 12-factor principles.

- Repository: https://github.com/theskumar/python-dotenv
- Website: https://saurabh-kumar.com/python-dotenv/
- Stars: 8,883 · Forks: 585
- Language: Python
- License: BSD-3-Clause
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/theskumar-python-dotenv

## The problem python-dotenv solves for 12-factor Python apps

A 12-factor application takes its configuration from environment variables, and the README says so directly: launching such an app in development "is not very practical because you have to set those environment variables yourself." That is the entire gap python-dotenv fills. You put the values in a .env file at the project root, call load_dotenv() at startup, and the rest of your code reads os.environ or os.getenv as if the variables had been exported by the shell.

The audience is Python developers and, to a lesser degree, system administrators, which is how the package classifies itself in pyproject.toml. It is not aimed at teams that want a configuration hierarchy, schema validation or a secrets backend. The README's own framing is narrow: a file, a loader, and the environment. If you have ever written a shell script that exports six variables before running a Flask app, this replaces that script with a file you can keep out of version control.

The design keeps production untouched by default. load_dotenv() does not override variables that already exist in the environment, so a container that sets DATABASE_URL at deploy time wins over whatever the .env file says. That default is the reason the library is safe to leave in application code rather than gating it behind an if DEBUG check.

## How load_dotenv, dotenv_values and the parser fit together

The package has one parser and several entry points around it. load_dotenv() parses a file and writes the results into os.environ. dotenv_values() uses the same parsing but returns a plain dict and leaves the environment alone, which the README describes as enabling "advanced configuration management": you can merge several files and os.environ into one mapping yourself, choosing the precedence order explicitly.

Both functions accept a stream argument, so the source does not have to be the filesystem. The README's example builds an io.StringIO object and passes it in, which means configuration fetched over the network can go through the same parser. Reading from FIFOs on Unix is also supported according to the file format section.

File discovery is handled by find_dotenv, which searches the directory of the calling script and then walks upward. That upward walk is convenient and also a source of surprise: a stray .env two directories above your project will be found and loaded. If you care which file was read, pass the path explicitly rather than relying on the search.

The parser itself follows a Bash-like syntax that the README calls "not formally specified and still improves over time." Keys may be unquoted or single-quoted; values may be unquoted, single-quoted or double-quoted; spaces around keys, equals signs and values are ignored. Single-quoted values allow \\ and \', double-quoted values allow those plus \" and the usual control escapes such as \n and \t. Quoted values may span multiple lines, and the README shows that a literal newline inside quotes is equivalent to \n.

Variable expansion follows POSIX rules with a twist worth reading twice. With load_dotenv(override=True) or dotenv_values(), a referenced variable resolves first from the .env file, then from the environment, then from a provided default, then to the empty string. With load_dotenv(override=False) the first two swap: the environment wins over the file. Bare $DOMAIN is not expanded; you must write ${DOMAIN}. There is also an asymmetry in how a valueless key is handled: dotenv_values returns None for it, while load_dotenv ignores the key entirely. FOO= (with an equals sign) is different again and yields the empty string.

## Installing python-dotenv and a first real .env file

The package installs from PyPI. The README gives the plain form, and the CLI lives behind an extra so that the base install does not pull in click:

```bash
pip install python-dotenv
```

For the command-line tool you install the extra instead. The pyproject.toml declares click>=5.0 under the cli optional dependency and exposes a dotenv console script:

```bash
pip install "python-dotenv[cli]"
```

Now create a .env file at the root of your project. The README's example uses this shape, including a variable that references another one:

```bash
# Development settings
DOMAIN=example.org
ADMIN_EMAIL=admin@${DOMAIN}
ROOT_URL=${DOMAIN}/app
```

Load it before anything else reads configuration. The call sets the values in os.environ and, by default, leaves already-exported variables alone:

```python
from dotenv import load_dotenv

load_dotenv()  # reads variables from a .env file and sets them in os.environ
```

If you would rather not mutate the process environment, use dotenv_values and inspect the dict. The README shows the expected result for a two-key file:

```python
from dotenv import dotenv_values

config = dotenv_values(".env")  # config = {"USER": "foo", "EMAIL": "foo@example.org"}
```

The CLI covers the cases where you want to edit or inspect the file without opening an editor. These commands come straight from the README:

```bash
dotenv set USER foo
dotenv list
dotenv list --format=json
dotenv run -- python foo.py
```

Two operational details belong in the first five minutes. Add .env to .gitignore, since the README expects it to hold secrets such as a password. And if a third-party package calls load_dotenv() in a context where you do not want it, set PYTHON_DOTENV_DISABLED=1, which the README documents as disabling loading from both files and streams.

## Where python-dotenv is the wrong tool

The library returns strings and nothing else. There is no schema, no type coercion, no required-key check. A typo in a key name produces no error at load time; you find out when os.getenv returns None somewhere deep in the call stack. Projects that need validated, typed settings are better served by a layer on top, and the README's own related projects list points at environs and dynaconf for exactly that reason.

The override default is the second trap. Because load_dotenv() does not override, a developer who exports DATABASE_URL in their shell and then edits .env will see the shell value win and may spend time debugging a file that is being read correctly. The inverse mistake is calling load_dotenv(override=True) in application code, which inverts the precedence and lets a local file beat the deployment environment. Neither behaviour is wrong, but the choice has to be deliberate.

Secrets are the third boundary. A .env file is plaintext on disk, and the README's only guidance is to gitignore it. There is no encryption, no access control, no audit trail. Teams that need secret rotation or per-environment secret storage should treat this package as a local development convenience and keep production values in whatever secret store they already run.

Finally, the file format is explicitly not specified. The README states it "still improves over time," which means a .env file that parses today is not guaranteed to parse identically after a major release. If your deployment pipeline generates .env files programmatically, pin the version and test the generated file against it.

## python-dotenv compared with environs and dynaconf

Both alternatives appear in the README's related projects list, and the difference is architectural rather than cosmetic. python-dotenv parses text and hands you strings. environs is a wrapper around python-dotenv that adds typed accessors and validation, so reading an integer or a boolean becomes an explicit call that fails loudly when the value is malformed. If your problem is "the .env file is fine but nothing checks it," environs is the layer you are missing, and it still uses this parser underneath.

dynaconf takes a different route: it treats configuration as a layered system with multiple sources and environments, of which a .env file is one input among several. That is the right shape when you have development, staging and production settings that need to be selected and merged, and the wrong shape when you just want six variables available to a script.

The honest comparison is that python-dotenv has the smallest surface area of the three. It does one thing, it has no runtime dependencies in the base install, and it is the thing the other two build on. Choosing it means accepting that validation and layering are your responsibility. Choosing environs or dynaconf means accepting another dependency and a configuration model to learn.

## Maintenance, releases and the BSD-3-Clause licence

The repository is not archived and the last push was on 2026-08-23, so the project is being worked on. Releases are frequent enough to matter for pinning: v1.2.1 shipped on 2025-10-26, v1.2.2 on 2026-03-01, and v1.2.3 on 2026-08-16. The Makefile shows the release path is scripted, requiring a clean working tree on main, running ruff and pytest, then bumpversion with a part argument that defaults to patch. That is a small, conventional toolchain, and it means a patch release can appear without a long freeze.

Upgrade cost is low but not zero. The package requires Python 3.10 or newer and classifies support through 3.14 plus PyPy, so dropping an older interpreter is a real change if you are pinned below 3.10. Because the .env format is not formally specified, a minor bump is the place to watch for parser changes; the CHANGELOG.md at the repository root is the file to read before upgrading, and the Makefile's release target suggests the changelog is updated as part of cutting a version.

The licence is BSD-3-Clause, declared in both the LICENSE file and the pyproject.toml metadata. That is a permissive licence with a standard warranty disclaimer. It permits use in closed-source products and places few obligations on you beyond retaining the copyright notice and licence text. This is a description of the licence terms, not legal advice; if your organisation has a policy on third-party licences, route it through that process.

## Conclusion

Adopt python-dotenv if you run a 12-factor Python application locally and want a .env file to stand in for variables you would otherwise export by hand; it is a development aid, not a secrets manager. Skip it if you need typed validation, layered config from many sources, or per-environment secret storage, and look at environs or dynaconf instead. Before you commit to it, verify three things in your own tree: that no .env file is tracked by git, that PYTHON_DOTENV_DISABLED=1 is set anywhere the file must not be read, and that your code does not depend on load_dotenv overriding variables that are already exported.

## FAQ

### What does python-dotenv do in Python?

It reads key-value pairs from a .env file and can set them as environment variables in the running process. The README frames this as helping applications follow 12-factor principles, where configuration comes from the environment rather than from code.

### How do I install python-dotenv?

Install it from PyPI with pip install python-dotenv. If you also want the dotenv command-line tool, install the extra instead with pip install "python-dotenv[cli]", which pulls in click>=5.0.

### How do I use python-dotenv in my code?

Import load_dotenv from dotenv and call it before your application reads configuration; it parses the .env file and writes the values into os.environ. If you prefer not to modify the environment, dotenv_values returns the parsed pairs as a dict instead.

### What is the difference between python-dotenv and dotenv?

python-dotenv is the Python package described here, distributed on PyPI and installed with pip. The README does not compare itself with other projects that share the dotenv name, so the distinction has to be drawn from the package name and the Python import path rather than from the documentation.

### Why should .env files not be pushed to a repository?

The README advises adding .env to .gitignore, especially when it contains secrets such as a password. The file is plaintext and the library offers no encryption or access control, so committing it exposes those values to everyone with repository access.

## Sources

- [License: BSD-3-Clause](https://github.com/theskumar/python-dotenv/blob/main/LICENSE)
- [Project website](https://saurabh-kumar.com/python-dotenv/)
- [README](https://github.com/theskumar/python-dotenv/blob/main/README.md)
- [Releases](https://github.com/theskumar/python-dotenv/releases)
- [theskumar/python-dotenv on GitHub](https://github.com/theskumar/python-dotenv)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/theskumar-python-dotenv
