python-dotenv review
python-dotenv 1.2.3 reads shell-like key and value lines from a `.env` file, then either places them in `os.environ` or returns them as a dictionary. It is a local configuration adapter for programs already designed around environment variables, with extra support for streams, upward file discovery, file editing, IPython, and a command wrapper. It does not validate types or protect secrets at rest. Version 1.2.3 strips a UTF-8 BOM before parsing the first key, preserves backslashes through `set_key()`, caches an empty parsed file, and gives `dotenv run` a clean missing-command error. Our pure-Python package shipped `py.typed` and imported as `dotenv`, not `python_dotenv`.
python-dotenv 1.2.3 installed in 0.3 seconds as one 1 MB typed package with no audit findings in our sandbox, so its runtime cost is tiny for local environment loading. Install it as a development adapter, keep paths and precedence explicit, and use a real secret store for production credentials.
We installed it
| Install | ✓ · 0.3s | 1 package on disk · 1 MB |
| Import | ✓ | import dotenv in 0.25s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does python-dotenv install cleanly?
Yes. In a fresh container with an empty cache, pip install python-dotenv finished in 0.3s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does python-dotenv need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import dotenv succeeded in 0.25s, and the package ships py.typed for type checkers.
python-dotenv or pydantic-settings: which should you use?
pydantic-settings: Use it for typed and validated settings models that can include dotenv as one source. python-dotenv 1.2.3 installed in 0.3 seconds as one 1 MB typed package with no audit findings in our sandbox, so its runtime cost is tiny for local environment loading.
When should you not use python-dotenv?
Settings require types, mandatory fields, nested structures, and useful validation errors. pydantic-settings owns those concerns.
Use it if
- The application already consumes `os.environ`, and local developers need to supply the same variables from one ignored file.
- Several dotenv files must be parsed into mappings and combined under a precedence order your code can show explicitly.
- Configuration text arrives through an in-memory stream or FIFO and should be parsed without creating a temporary regular file.
- A local command needs a selected dotenv file applied to its child process through the optional CLI.
- Settings require types, mandatory fields, nested structures, and useful validation errors. `pydantic-settings` owns those concerns.
- The configuration system spans named environments, several file formats, and remote secret providers. Dynaconf covers that broader model.
- You need secret storage. A `.env` file is plaintext and gains no encryption or access control from python-dotenv.
- A library, notebook, worker, or packaged executable cannot predict its working directory. Implicit upward search may select the wrong file; accept host configuration or pass an exact path.
- The same file must parse identically in Bash, Docker Compose, and multiple dotenv implementations. Their quoting, interpolation, multiline, and `export` rules differ.
- The deployment platform already injects every variable and third-party code does not call `load_dotenv()`. Production has no reason to add a file loader.
Setup reality
We installed python-dotenv 1.2.3 in a fresh unprivileged Python 3.12 Bookworm sandbox. pip took 0.3 seconds, and one installed package used 1 MB. import dotenv completed in 0.25 seconds. The distribution is pure Python, includes py.typed, declares one direct dependency, requires Python 3.10+, and uses the BSD-3-Clause license. pip-audit found zero known vulnerabilities in our resolved environment.
Load the file before importing modules that read variables at module scope. load_dotenv() preserves an existing process value unless override=True is set. Its automatic finder walks upward, which makes the chosen file depend on script location and runtime context. Services and tests are easier to reason about when they construct a path from a known application root.
Every result is a string, None, or absence. KEY and KEY= are different: dictionary parsing maps the first form to None and the second to an empty string, while environment loading ignores a valueless key. Expansion recognizes ${DOMAIN}, not bare $DOMAIN. Version 1.2.3 now removes a leading UTF-8 BOM and keeps backslashes intact through set_key(), fixing two Windows-heavy failure modes.
Use dotenv_values() when parsing should not mutate global process state. Merge mappings in visible order and put os.environ last if deployed values must win. Editing functions rewrite a shared file and need external coordination when several processes can write. The dotenv command comes from the cli extra. On a host that already injects variables, omit file loading or set PYTHON_DOTENV_DISABLED=1 when code outside your control calls it.
Patterns
Load variables from the application directory load-fixed-path
import os
from pathlib import Path
from dotenv import load_dotenv
root = Path(__file__).resolve().parent
load_dotenv(root / ".env")
database_url = os.environ["DATABASE_URL"]Call this before imports that read environment values. A path anchored to source avoids selection changes when the process starts elsewhere.
Keep host-injected values ahead of file values preserve-process-environment
from dotenv import load_dotenv
load_dotenv(".env", override=False)`False` is the default. Any name already present in `os.environ` remains untouched.
Apply a controlled test file with higher priority override-process-environment
from dotenv import load_dotenv
load_dotenv(".env.test", override=True)This changes process-global state. Restore affected keys in test cleanup when other cases share the same interpreter.
Read a file without touching `os.environ` parse-to-dictionary
from dotenv import dotenv_values
values = dotenv_values(".env")
api_url = values.get("API_URL")A line containing only a name can map to `None`; `NAME=` maps to an empty string. Decide whether either form is valid configuration.
Put host variables after file defaults layer-configuration
import os
from dotenv import dotenv_values
config = {
**dotenv_values(".env.shared"),
**dotenv_values(".env.local"),
**os.environ,
}Dictionary order defines precedence here. With `os.environ` last, deployed variables replace both shared and local file entries.
Read dotenv content held in memory parse-memory-stream
from io import StringIO
from dotenv import dotenv_values
config = dotenv_values(stream=StringIO("MODE=worker\nPORT=9000\n"))Stream parsing avoids a temporary file. The caller still owns transport security, logging policy, and lifetime of the plaintext content.
Interpolate a previously defined value expand-file-variable
# .env
DOMAIN=example.com
API_URL=https://${DOMAIN}/apiBraces are required. python-dotenv leaves `$DOMAIN` unchanged, while `${DOMAIN}` participates in its documented expansion order.
Find a file relative to process startup search-from-current-directory
from dotenv import find_dotenv, load_dotenv
path = find_dotenv(usecwd=True)
if path:
load_dotenv(path)`usecwd=True` makes the search depend on the current directory. It suits an interactive CLI but is risky for services launched from varying paths.
Start a child process with a selected file run-child-command
python -m pip install "python-dotenv[cli]"
dotenv -f .env.local run -- python app.pyThe `dotenv` executable is installed by the `cli` extra. Review its override flags before placing this command in shared automation.
Block loading in a deployed process disable-dotenv-read
export PYTHON_DOTENV_DISABLED=1
python app.pyThis switch makes `load_dotenv()` ignore files and streams, including calls made by third-party packages that cannot be edited.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pydantic-settings | PyPI | Use it for typed and validated settings models that can include dotenv as one source. |
| environs | PyPI | Use it for environment parsing and casting without building Pydantic models. |
| dynaconf | PyPI | Use it for named environments, layered file formats, and external secret backends. |
More utils guides
lru-cache · type-fest · ajv · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

