diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index dc91d369..6cda045b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -38,7 +38,7 @@ jobs:
run: |
pip install -r requirements-docs.txt
pip install -e .
- mkdocs build
+ zensical build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
diff --git a/.gitignore b/.gitignore
index ba1234ce..a162a4c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,16 +67,6 @@ coverage.xml
local_settings.py
db.sqlite3
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
# PyBuilder
target/
@@ -90,9 +80,6 @@ ipython_config.py
# pyenv
.python-version
-# celery beat schedule file
-celerybeat-schedule
-
# SageMath parsed files
*.sage.py
@@ -112,7 +99,7 @@ venv.bak/
# Rope project settings
.ropeproject
-# mkdocs documentation
+# zensical documentation
/site
# mypy
@@ -125,5 +112,6 @@ dmypy.json
### Python Patch ###
.venv/
+uv.lock
# End of https://www.gitignore.io/api/python
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee732346..ca7de0fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -488,6 +488,9 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]).
[@JYOuyang]: https://github.com/JYOuyang
[@burnout-projects]: https://github.com/burnout-projects
[@cpackham-atlnz]: https://github.com/cpackham-atlnz
+
+
+
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.2.2...HEAD
[1.2.2]: https://github.com/theskumar/python-dotenv/compare/v1.2.1...v1.2.2
[1.2.1]: https://github.com/theskumar/python-dotenv/compare/v1.2.0...v1.2.1
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 49840fa7..add53f3b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,5 +1,4 @@
-Contributing
-============
+# Contributing
All the contributions are welcome! Please open [an
issue](https://github.com/theskumar/python-dotenv/issues/new) or send us
@@ -18,18 +17,15 @@ or with [tox](https://pypi.org/project/tox/) installed:
$ tox
-
Use of pre-commit is recommended:
$ uv run precommit install
-
-Documentation is published with [mkdocs]():
+Documentation is published with [zensical](https://zensical.org/) and follows
+the approach established by [Diátaxis](https://diataxis.fr/):
```shell
-$ uv pip install -r requirements-docs.txt
-$ uv pip install -e .
-$ uv run mkdocs serve
+$ make serve-docs
```
Open http://127.0.0.1:8000/ to view the documentation locally.
diff --git a/Makefile b/Makefile
index 78866a60..23573f6b 100644
--- a/Makefile
+++ b/Makefile
@@ -33,3 +33,8 @@ coverage:
coverage-html: coverage
coverage html
+
+serve-docs:
+ uv pip install -r requirements-docs.txt
+ uv pip install -e .
+ uv run zensical serve
diff --git a/README.md b/README.md
index a08d6141..98659329 100644
--- a/README.md
+++ b/README.md
@@ -7,17 +7,7 @@ python-dotenv reads key-value pairs from a `.env` file and can set them as
environment variables. It helps in the development of applications following the
[12-factor](https://12factor.net/) principles.
-- [Getting Started](#getting-started)
-- [Other Use Cases](#other-use-cases)
- - [Load configuration without altering the environment](#load-configuration-without-altering-the-environment)
- - [Parse configuration as a stream](#parse-configuration-as-a-stream)
- - [Load .env files in IPython](#load-env-files-in-ipython)
-- [Command-line Interface](#command-line-interface)
-- [File format](#file-format)
- - [Multiline values](#multiline-values)
- - [Variable expansion](#variable-expansion)
-- [Related Projects](#related-projects)
-- [Acknowledgements](#acknowledgements)
+> **[Read the full documentation](https://theskumar.github.io/python-dotenv/)**
## Getting Started
@@ -73,163 +63,9 @@ like `${DOMAIN}`, as bare variables such as `$DOMAIN` are not expanded.
You will probably want to add `.env` to your `.gitignore`, especially if it
contains secrets like a password.
-See the section "[File format](#file-format)" below for more information about what you can write in a `.env` file.
-
-## Other Use Cases
-
-### Load configuration without altering the environment
-
-The function `dotenv_values` works more or less the same way as `load_dotenv`,
-except it doesn't touch the environment, it just returns a `dict` with the
-values parsed from the `.env` file.
-
-```python
-from dotenv import dotenv_values
-
-config = dotenv_values(".env") # config = {"USER": "foo", "EMAIL": "foo@example.org"}
-```
-
-This notably enables advanced configuration management:
-
-```python
-import os
-from dotenv import dotenv_values
-
-config = {
- **dotenv_values(".env.shared"), # load shared development variables
- **dotenv_values(".env.secret"), # load sensitive variables
- **os.environ, # override loaded values with environment variables
-}
-```
-
-### Parse configuration as a stream
-
-`load_dotenv` and `dotenv_values` accept [streams][python_streams] via their
-`stream` argument. It is thus possible to load the variables from sources other
-than the filesystem (e.g. the network).
-
-```python
-from io import StringIO
-
-from dotenv import load_dotenv
-
-config = StringIO("USER=foo\nEMAIL=foo@example.org")
-load_dotenv(stream=config)
-```
-
-### Load .env files in IPython
-
-You can use dotenv in IPython. By default, it will use `find_dotenv` to search for a
-`.env` file:
-
-```python
-%load_ext dotenv
-%dotenv
-```
-
-You can also specify a path:
-
-```python
-%dotenv relative/or/absolute/path/to/.env
-```
-
-Optional flags:
-
-- `-o` to override existing variables.
-- `-v` for increased verbosity.
-
-### Disable load_dotenv
-
-Set `PYTHON_DOTENV_DISABLED=1` to disable `load_dotenv()` from loading .env
-files or streams. Useful when you can't modify third-party package calls or in
-production.
-
-## Command-line Interface
-
-A CLI interface `dotenv` is also included, which helps you manipulate the `.env`
-file without manually opening it.
-
-```shell
-$ pip install "python-dotenv[cli]"
-$ dotenv set USER foo
-$ dotenv set EMAIL foo@example.org
-$ dotenv list
-USER=foo
-EMAIL=foo@example.org
-$ dotenv list --format=json
-{
- "USER": "foo",
- "EMAIL": "foo@example.org"
-}
-$ dotenv run -- python foo.py
-```
-
-Run `dotenv --help` for more information about the options and subcommands.
-
-## File format
-
-The format is not formally specified and still improves over time. That being
-said, `.env` files should mostly look like Bash files. Reading from FIFOs (named
-pipes) on Unix systems is also supported.
-
-Keys can be unquoted or single-quoted. Values can be unquoted, single- or
-double-quoted. Spaces before and after keys, equal signs, and values are
-ignored. Values can be followed by a comment. Lines can start with the `export`
-directive, which does not affect their interpretation.
-
-Allowed escape sequences:
-
-- in single-quoted values: `\\`, `\'`
-- in double-quoted values: `\\`, `\'`, `\"`, `\a`, `\b`, `\f`, `\n`, `\r`, `\t`, `\v`
-
-### Multiline values
-
-It is possible for single- or double-quoted values to span multiple lines. The
-following examples are equivalent:
-
-```bash
-FOO="first line
-second line"
-```
-
-```bash
-FOO="first line\nsecond line"
-```
-
-### Variable without a value
-
-A variable can have no value:
-
-```bash
-FOO
-```
-
-It results in `dotenv_values` associating that variable name with the value
-`None` (e.g. `{"FOO": None}`. `load_dotenv`, on the other hand, simply ignores
-such variables.
-
-This shouldn't be confused with `FOO=`, in which case the variable is associated
-with the empty string.
-
-### Variable expansion
-
-python-dotenv can interpolate variables using POSIX variable expansion.
-
-With `load_dotenv(override=True)` or `dotenv_values()`, the value of a variable
-is the first of the values defined in the following list:
-
-- Value of that variable in the `.env` file.
-- Value of that variable in the environment.
-- Default value, if provided.
-- Empty string.
-
-With `load_dotenv(override=False)`, the value of a variable is the first of the
-values defined in the following list:
-
-- Value of that variable in the environment.
-- Value of that variable in the `.env` file.
-- Default value, if provided.
-- Empty string.
+See the [file format specification](https://theskumar.github.io/python-dotenv/reference/file-format/)
+for full details on `.env` syntax, multiline values, escape sequences, and
+variable expansion.
## Related Projects
diff --git a/docs/changelog.md b/docs/changelog.md
deleted file mode 120000
index 04c99a55..00000000
--- a/docs/changelog.md
+++ /dev/null
@@ -1 +0,0 @@
-../CHANGELOG.md
\ No newline at end of file
diff --git a/docs/changelog.md b/docs/changelog.md
new file mode 100644
index 00000000..786b75d5
--- /dev/null
+++ b/docs/changelog.md
@@ -0,0 +1 @@
+--8<-- "CHANGELOG.md"
diff --git a/docs/contributing.md b/docs/contributing.md
deleted file mode 120000
index 44fcc634..00000000
--- a/docs/contributing.md
+++ /dev/null
@@ -1 +0,0 @@
-../CONTRIBUTING.md
\ No newline at end of file
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 00000000..383d6158
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1 @@
+--8<-- "contributing.md"
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 00000000..433600a8
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,88 @@
+---
+icon: lucide/rocket
+---
+
+# Getting Started
+
+This tutorial walks you through installing python-dotenv and using it to load
+environment variables from a `.env` file.
+
+## Installation
+
+=== "`pip`"
+
+ ```shell
+ pip install python-dotenv
+ ```
+
+=== "`uv`"
+
+ ```shell
+ uv add python-dotenv
+ ```
+
+## Create a `.env` file
+
+Add a `.env` file to the root of your project:
+
+```
+.
+├── .env
+└── app.py
+```
+
+Write your configuration as key-value pairs:
+
+```bash
+# .env
+DOMAIN=example.org
+ADMIN_EMAIL=admin@${DOMAIN}
+ROOT_URL=${DOMAIN}/app
+```
+
+If you use variables in values, ensure they are surrounded with `{` and `}`,
+like `${DOMAIN}`, as bare variables such as `$DOMAIN` are not expanded. See
+[Variable expansion](reference/file-format.md#variable-expansion) for the full
+syntax.
+
+!!! tip
+
+ Add `.env` to your `.gitignore`, especially if it contains secrets.
+
+## Load the `.env` file
+
+In your Python application:
+
+```python
+from dotenv import load_dotenv
+import os
+
+load_dotenv()
+
+domain = os.getenv("DOMAIN")
+print(domain) # example.org
+```
+
+`load_dotenv()` looks for a `.env` file starting in the same directory as the
+calling script, walking up the directory tree until it finds one. Each key-value
+pair is added to `os.environ`.
+
+## How it works
+
+By default, `load_dotenv()`:
+
+- Searches for a `.env` file automatically using `find_dotenv()`.
+- Reads each key-value pair and adds it to `os.environ`.
+- **Does not override** existing environment variables. Pass `override=True` to change this.
+
+```python
+# Override existing environment variables
+load_dotenv(override=True)
+```
+
+## Next steps
+
+- [Configuration Patterns](guides/configuration.md): load config as a dict, layer multiple `.env` files.
+- [Command-line Interface](guides/cli.md): manage `.env` files and run commands from the terminal.
+- [File Format](reference/file-format.md): full specification of `.env` file syntax.
+- [Python API Reference](reference/api.md): all functions and their parameters.
diff --git a/docs/guides/advanced.md b/docs/guides/advanced.md
new file mode 100644
index 00000000..65931139
--- /dev/null
+++ b/docs/guides/advanced.md
@@ -0,0 +1,129 @@
+---
+icon: lucide/atom
+---
+
+# Advanced Usage
+
+## Parse configuration from a stream
+
+`load_dotenv` and `dotenv_values` accept text streams via the `stream` argument,
+allowing you to load variables from sources other than the filesystem:
+
+```python
+from io import StringIO
+from dotenv import load_dotenv
+
+config = StringIO("USER=foo\nEMAIL=foo@example.org")
+load_dotenv(stream=config)
+```
+
+This works with any text stream - `StringIO`, file objects, or responses from
+network calls.
+
+## Read from FIFOs (named pipes)
+
+On Unix systems, python-dotenv can read `.env` from a FIFO (named pipe). This is
+useful when the `.env` content is generated dynamically:
+
+```shell
+mkfifo .env
+echo "SECRET=dynamic_value" > .env &
+python app.py
+```
+
+`find_dotenv()` matches FIFOs in addition to regular files.
+
+## File encoding
+
+All functions that read or write `.env` files accept an `encoding` parameter
+(defaults to `utf-8`):
+
+```python
+from dotenv import load_dotenv, dotenv_values
+
+load_dotenv(".env", encoding="latin-1")
+
+config = dotenv_values(".env", encoding="latin-1")
+```
+
+## Disable `load_dotenv`
+
+Set the `PYTHON_DOTENV_DISABLED` environment variable to disable `load_dotenv()`
+from loading `.env` files or streams. Accepted truthy values: `1`, `true`, `t`,
+`yes`, `y` (case-insensitive).
+
+```shell
+PYTHON_DOTENV_DISABLED=1 python app.py
+```
+
+This is useful when you can't modify third-party package calls to `load_dotenv`,
+or in production environments where `.env` files should be ignored.
+
+## Controlling file discovery with `find_dotenv`
+
+`find_dotenv()` walks up the directory tree from the calling script's location
+until it finds a `.env` file:
+
+```python
+from dotenv import find_dotenv
+
+path = find_dotenv() # returns absolute path or ""
+```
+
+Use `usecwd=True` to search from the current working directory instead of the
+calling script's directory:
+
+```python
+path = find_dotenv(usecwd=True)
+```
+
+Search for a file with a different name:
+
+```python
+path = find_dotenv(".env.production")
+```
+
+Raise an error if the file is not found:
+
+```python
+path = find_dotenv(raise_error_if_not_found=True) # raises IOError
+```
+
+In REPL, IPython, frozen, and debugger environments, `find_dotenv` automatically
+uses the current working directory.
+
+## Symlink handling
+
+`set_key` and `unset_key` do **not** follow symlinks by default. If your `.env`
+is a symlink and you want writes to target the symlink destination, pass
+`follow_symlinks=True`:
+
+```python
+from dotenv import set_key
+
+set_key(".env", "KEY", "value", follow_symlinks=True)
+```
+
+!!! warning
+
+ This behavior changed in v1.2.2. Previously, symlinks were followed in some situations. See the [changelog](../changelog/#122-2026-03-01) for details.
+
+## Debugging with verbose mode
+
+Pass `verbose=True` to log warnings when files are missing or keys are not found:
+
+```python
+from dotenv import load_dotenv, get_key
+
+load_dotenv(verbose=True) # warns if .env is missing
+get_key(".env", "MISSING_KEY") # warns if key not found
+```
+
+Messages are emitted via Python's `logging` module under the `dotenv.main`
+logger. To see them, configure logging:
+
+```python
+import logging
+
+logging.basicConfig()
+```
diff --git a/docs/guides/cli.md b/docs/guides/cli.md
new file mode 100644
index 00000000..47d2a388
--- /dev/null
+++ b/docs/guides/cli.md
@@ -0,0 +1,85 @@
+---
+icon: lucide/square-terminal
+---
+
+# Command-line Interface
+
+python-dotenv includes a `dotenv` CLI for managing `.env` files from the
+terminal.
+
+## Installation
+
+The CLI requires the `click` package. Install it with the `cli` extra:
+
+=== "`pip`"
+
+ ```shell
+ pip install "python-dotenv[cli]"
+ ```
+
+=== "`uv`"
+
+ ```shell
+ uv add "python-dotenv[cli]"
+ ```
+
+Verify the installation by checking the version:
+
+```shell
+dotenv --version
+```
+
+!!! info
+
+ The CLI can also be invoked as a Python module: `python -m dotenv`
+
+## Set up your .env from the terminal
+
+Use `set`, `get`, and `unset` to manage key-value pairs:
+
+```shell
+dotenv set USER foo
+dotenv set EMAIL foo@example.org
+dotenv get EMAIL
+dotenv unset EMAIL
+```
+
+See [CLI Reference](../reference/cli.md) for options such as `--quote` and
+`--export`.
+
+## Run a command with .env loaded
+
+Load environment variables from `.env` and execute a command:
+
+```shell
+dotenv run -- python app.py
+```
+
+By default, `.env` values override existing environment variables. Use
+`--no-override` to keep existing values. See [CLI
+Reference](../reference/cli.md#dotenv-run) for details.
+
+## Export .env for shell use
+
+List values in a format suitable for sourcing in a shell:
+
+```shell
+dotenv list --format=export
+```
+
+Other formats include `simple`, `json`, and `shell`. See [CLI
+Reference](../reference/cli.md#dotenv-list) for all options.
+
+## Use a non-default .env file
+
+Use `-f` to target a specific file (default is `.env` in the current directory):
+
+```shell
+dotenv -f .env.production list
+dotenv -f .env.production run -- python app.py
+```
+
+## Full CLI reference
+
+See [CLI Reference](../reference/cli.md) for all commands, options, and exit
+codes.
diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md
new file mode 100644
index 00000000..ecec20e2
--- /dev/null
+++ b/docs/guides/configuration.md
@@ -0,0 +1,195 @@
+---
+icon: lucide/bolt
+---
+
+# Configuration Patterns
+
+## Load configuration without altering the environment
+
+`dotenv_values` parses a `.env` file and returns a `dict` without modifying
+`os.environ`:
+
+```python
+from dotenv import dotenv_values
+
+config = dotenv_values(".env")
+# config = {"USER": "foo", "EMAIL": "foo@example.org"}
+```
+
+This is useful when you need the values but don't want to pollute the global
+environment, for example in tests or when passing config explicitly to your
+application.
+
+## Per-environment files
+
+Use separate `.env` files for each environment. Load a base file first, then
+apply environment-specific overrides:
+
+```python
+import os
+from dotenv import load_dotenv
+
+load_dotenv(".env") # shared defaults
+
+env = os.getenv("APP_ENV", "development")
+load_dotenv(f".env.{env}", override=True) # environment-specific overrides
+```
+
+`load_dotenv` silently does nothing if the file doesn't exist, so this is safe
+to use even when only some override files are present.
+
+A typical file layout:
+
+```
+.env # shared defaults (committed)
+.env.development # local dev overrides (gitignored)
+.env.test # test overrides (committed or gitignored)
+.env.production # production overrides (gitignored, or use real env vars)
+```
+
+The same pattern works with `dotenv_values`:
+
+```python
+import os
+from dotenv import dotenv_values
+
+env = os.getenv("APP_ENV", "development")
+config = {
+ **dotenv_values(".env"), # shared defaults
+ **dotenv_values(f".env.{env}"), # environment overrides
+ **os.environ, # real environment wins last
+}
+```
+
+## Layer multiple `.env` files
+
+Combine multiple `.env` files with environment variables for layered
+configuration:
+
+```python
+import os
+from dotenv import dotenv_values
+
+config = {
+ **dotenv_values(".env.shared"), # shared development variables
+ **dotenv_values(".env.secret"), # sensitive variables
+ **os.environ, # override with real environment
+}
+```
+
+Later sources override earlier ones, so environment variables take highest
+priority.
+
+## Get, set, and unset individual keys
+
+Read a single value from a `.env` file:
+
+```python
+from dotenv import get_key
+
+value = get_key(".env", "EMAIL")
+```
+
+Write a key-value pair (creates the file if it doesn't exist):
+
+```python
+from dotenv import set_key
+
+set_key(".env", "EMAIL", "foo@example.org")
+```
+
+Remove a key:
+
+```python
+from dotenv import unset_key
+
+unset_key(".env", "EMAIL")
+```
+
+## Override behavior
+
+By default, `load_dotenv` does **not** override existing environment variables:
+
+```python
+from dotenv import load_dotenv
+
+load_dotenv() # existing env wins
+load_dotenv(override=True) # .env wins
+```
+
+`dotenv_values` always resolves as if `override=True`.
+
+See [Resolution order](../reference/file-format.md#resolution-order) for the
+full precedence rules.
+
+## Disable variable interpolation
+
+By default, `${VAR}` references in values are resolved. To treat values as
+literal strings, disable interpolation:
+
+```python
+from dotenv import load_dotenv, dotenv_values
+
+load_dotenv(interpolate=False)
+
+# or
+config = dotenv_values(".env", interpolate=False)
+```
+
+This is useful when values contain `$` characters that should not be expanded,
+such as passwords.
+
+## Type casting
+
+python-dotenv returns all values as strings. Cast them in your application code:
+
+```python
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
+
+# Integer
+db_port = int(os.getenv("DB_PORT", "5432"))
+
+# Boolean - careful, bool("false") is True
+debug = os.getenv("DEBUG", "false").lower() in ("1", "true", "yes")
+
+# Comma-separated list
+allowed_hosts = os.getenv("ALLOWED_HOSTS", "").split(",")
+```
+
+For projects with many typed settings, consider
+[pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)
+or [python-decouple](https://github.com/HBNetwork/python-decouple), which handle
+type casting and validation declaratively.
+
+## Use a `.env.example` template
+
+Commit a `.env.example` file with all required keys but no secret values. This
+documents the expected configuration and helps with team onboarding:
+
+```bash
+# .env.example - copy to .env and fill in values
+SECRET_KEY=change-me
+DATABASE_URL=postgresql://user:password@localhost:5432/mydb
+
+# Optional (defaults shown)
+DEBUG=false
+LOG_LEVEL=INFO
+```
+
+New developers can then run:
+
+```shell
+cp .env.example .env
+```
+
+Add `.env` to `.gitignore` but keep `.env.example` committed:
+
+```gitignore
+.env
+.env.production
+.env.*.local
+!.env.example
+```
diff --git a/docs/guides/index.md b/docs/guides/index.md
new file mode 100644
index 00000000..017ea2dc
--- /dev/null
+++ b/docs/guides/index.md
@@ -0,0 +1,8 @@
+# How-to Guides
+
+Task-oriented recipes for common python-dotenv workflows.
+
+- [**Configuration Patterns**](configuration.md): Load config as a dict, layer multiple `.env` files, get/set/unset keys.
+- [**Command-line Interface**](cli.md): Manage `.env` files and run commands from the terminal.
+- [**IPython & Jupyter**](ipython.md): Use the `%dotenv` magic command in interactive sessions.
+- [**Advanced Usage**](advanced.md): Streams, FIFOs, disabling `load_dotenv`, and symlink handling.
diff --git a/docs/guides/ipython.md b/docs/guides/ipython.md
new file mode 100644
index 00000000..ca87cff2
--- /dev/null
+++ b/docs/guides/ipython.md
@@ -0,0 +1,39 @@
+---
+icon: lucide/notebook
+---
+
+# IPython & Jupyter
+
+python-dotenv provides a `%dotenv` magic command for IPython and Jupyter sessions.
+
+## Loading the extension
+
+```python
+%load_ext dotenv
+```
+
+## Usage
+
+Load `.env` from the default location (found automatically via `find_dotenv`):
+
+```python
+%dotenv
+```
+
+Load from a specific path:
+
+```python
+%dotenv relative/or/absolute/path/to/.env
+```
+
+## Flags
+
+`-o`
+: Override existing environment variables.
+
+`-v`
+: Verbose output. Prints each variable as it is loaded.
+
+```python
+%dotenv -o -v
+```
diff --git a/docs/index.md b/docs/index.md
deleted file mode 120000
index 32d46ee8..00000000
--- a/docs/index.md
+++ /dev/null
@@ -1 +0,0 @@
-../README.md
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..b4a70492
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,53 @@
+# python-dotenv
+
+[![Build Status][build_status_badge]][build_status_link]
+[![PyPI version][pypi_badge]][pypi_link]
+
+python-dotenv reads key-value pairs from a `.env` file and sets them as
+environment variables. It helps in the development of applications following the
+[12-factor](https://12factor.net/) principles.
+
+=== "`pip`"
+
+ ```shell
+ pip install python-dotenv
+ ```
+
+=== "`uv`"
+
+ ```shell
+ uv add python-dotenv
+ ```
+
+```python
+from dotenv import load_dotenv
+
+load_dotenv() # takes variables from .env and adds them to os.environ
+```
+
+## Documentation
+
+
+
+- [**Getting Started**](getting-started.md) →
+
+ Install python-dotenv and load your first `.env` file.
+
+- [**How-to Guides**](guides/index.md) →
+
+ Recipes for configuration patterns, the CLI, IPython, and advanced usage.
+
+- [**Reference**](reference/index.md) →
+
+ Complete Python API, CLI reference, and `.env` file format specification.
+
+- [**Changelog**](changelog.md) →
+
+ Release history and upgrade notes.
+
+
+
+[pypi_link]: https://badge.fury.io/py/python-dotenv
+[pypi_badge]: https://badge.fury.io/py/python-dotenv.svg
+[build_status_link]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml
+[build_status_badge]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml/badge.svg
diff --git a/docs/license.md b/docs/license.md
deleted file mode 120000
index ea5b6064..00000000
--- a/docs/license.md
+++ /dev/null
@@ -1 +0,0 @@
-../LICENSE
\ No newline at end of file
diff --git a/docs/license.md b/docs/license.md
new file mode 100644
index 00000000..f409d452
--- /dev/null
+++ b/docs/license.md
@@ -0,0 +1 @@
+--8<-- "LICENSE"
diff --git a/docs/reference.md b/docs/reference.md
deleted file mode 100644
index 8a3762ad..00000000
--- a/docs/reference.md
+++ /dev/null
@@ -1,2 +0,0 @@
-# ::: dotenv
-
diff --git a/docs/reference/api.md b/docs/reference/api.md
new file mode 100644
index 00000000..bae8a1bc
--- /dev/null
+++ b/docs/reference/api.md
@@ -0,0 +1,19 @@
+---
+icon: lucide/code
+---
+
+# Python API
+
+::: dotenv.load_dotenv
+
+::: dotenv.dotenv_values
+
+::: dotenv.find_dotenv
+
+::: dotenv.get_key
+
+::: dotenv.set_key
+
+::: dotenv.unset_key
+
+::: dotenv.get_cli_string
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
new file mode 100644
index 00000000..6e78f438
--- /dev/null
+++ b/docs/reference/cli.md
@@ -0,0 +1,99 @@
+---
+icon: lucide/square-terminal
+---
+
+# CLI Reference
+
+Requires the `cli` extra. See [CLI Guide](../guides/cli.md#installation) for installation.
+
+## Global options
+
+| Option | Short | Default | Description |
+| --------------- | ----- | -------- | ------------------------------------------------------------------------------------------ |
+| `--file PATH` | `-f` | `.env` | Location of the `.env` file in the current working directory. |
+| `--quote MODE` | `-q` | `always` | Quote mode for values when writing: `always`, `never`, or `auto`. Does not affect parsing. |
+| `--export BOOL` | `-e` | `false` | Prepend `export` to entries when writing with `set`. |
+| `--version` | | | Show the version and exit. |
+
+## Commands
+
+### `dotenv list`
+
+Display all stored key-value pairs. Output is sorted alphabetically by key. Keys with no value (`None`) are excluded.
+
+```shell
+dotenv list
+dotenv list --format=json
+```
+
+| Option | Values | Default | Description |
+| ---------- | ----------------------------------- | -------- | -------------- |
+| `--format` | `simple`, `json`, `shell`, `export` | `simple` | Output format. |
+
+**Formats:**
+
+- `simple`: `KEY=value` (no quotes)
+- `json`: JSON object with indentation, sorted by key
+- `shell`: Shell-escaped values (uses `shlex.quote`)
+- `export`: Prefixed with `export`, shell-escaped values
+
+### `dotenv set`
+
+Store a key-value pair. Creates the file if it doesn't exist.
+
+```shell
+dotenv set KEY value
+```
+
+Respects the global `--quote` and `--export` options. Does not follow symlinks.
+
+**Exit code 1** on failure.
+
+### `dotenv get`
+
+Retrieve the value for a key.
+
+```shell
+dotenv get KEY
+```
+
+**Exit code 1** if the key is not found or has no value.
+
+### `dotenv unset`
+
+Remove a key from the `.env` file.
+
+```shell
+dotenv unset KEY
+```
+
+Respects the global `--quote` option. Does not follow symlinks.
+
+**Exit code 1** on failure.
+
+### `dotenv run`
+
+Run a command with the `.env` file loaded into the environment.
+
+```shell
+dotenv run -- python app.py
+dotenv run --no-override -- python app.py
+```
+
+| Option | Default | Description |
+| ------------------------------ | ------------ | -------------------------------------------------------------- |
+| `--override` / `--no-override` | `--override` | Whether `.env` values override existing environment variables. |
+
+The `.env` file must exist. The command fails with an error if it does not.
+
+**Exit code 1** if no command is given.
+
+On Unix, `dotenv run` replaces the current process with `os.execvpe`. On Windows, it spawns a subprocess.
+
+## Exit codes
+
+| Code | Meaning |
+| ---- | ----------------------------------------------------------------------------------------------------------- |
+| `0` | Success. |
+| `1` | Command-specific error: key not found (`get`), write failure (`set`, `unset`), or no command given (`run`). |
+| `2` | Could not open the `.env` file. |
diff --git a/docs/reference/file-format.md b/docs/reference/file-format.md
new file mode 100644
index 00000000..42078a64
--- /dev/null
+++ b/docs/reference/file-format.md
@@ -0,0 +1,138 @@
+---
+icon: lucide/file-text
+---
+
+# File Format
+
+The format is not formally specified and still improves over time. That being
+said, `.env` files should mostly look like Bash files. Reading from FIFOs (named
+pipes) on Unix systems is also supported.
+
+## Keys and values
+
+Keys can be unquoted or single-quoted. Values can be unquoted, single-quoted,
+or double-quoted. Spaces before and after keys, equal signs, and values are
+ignored.
+
+```bash
+# Unquoted
+USER=foo
+
+# Single-quoted key and value
+'USER'='foo'
+
+# Double-quoted value
+USER="foo"
+```
+
+Lines can start with the `export` directive, which does not affect their
+interpretation:
+
+```bash
+export USER=foo
+```
+
+## Comments
+
+Lines starting with `#` are comments:
+
+```bash
+# This is a comment
+USER=foo
+```
+
+Unquoted values can have inline comments after whitespace:
+
+```bash
+USER=foo # this is a comment
+```
+
+Inline comments are not supported in quoted values. The `#` becomes part of
+the value:
+
+```bash
+USER="foo # this is NOT a comment"
+```
+
+## Escape sequences
+
+**In single-quoted values:** `\\`, `\'`
+
+**In double-quoted values:** `\\`, `\'`, `\"`, `\a`, `\b`, `\f`, `\n`, `\r`, `\t`, `\v`
+
+Unquoted values do not support escape sequences.
+
+## Multiline values
+
+Single- or double-quoted values can span multiple lines. The following are
+equivalent:
+
+```bash
+FOO="first line
+second line"
+```
+
+```bash
+FOO="first line\nsecond line"
+```
+
+Unquoted values cannot span multiple lines.
+
+## Variable without a value
+
+A variable can have no value:
+
+```bash
+FOO
+```
+
+`dotenv_values` returns `None` for such keys (e.g. `{"FOO": None}`).
+`load_dotenv` ignores them.
+
+This is different from `FOO=`, which sets the variable to an empty string.
+
+## Variable expansion
+
+python-dotenv can interpolate variables using POSIX variable expansion.
+
+Variables must use the `${VAR}` syntax with braces:
+
+```bash
+DOMAIN=example.org
+ADMIN_EMAIL=admin@${DOMAIN}
+```
+
+Bare variables like `$DOMAIN` (without braces) are **not** expanded.
+
+Default values are supported with `${VAR:-default}`:
+
+```bash
+DATABASE_HOST=${DB_HOST:-localhost}
+DATABASE_URL=postgres://${DATABASE_HOST}:5432/mydb
+```
+
+If `DB_HOST` is not defined in the `.env` file or the environment, `DATABASE_HOST`
+resolves to `localhost`.
+
+### Resolution order
+
+With `load_dotenv(override=True)` or `dotenv_values()`, the value of a variable
+is the first of the values defined in the following list:
+
+1. Value of that variable in the `.env` file.
+2. Value of that variable in the environment.
+3. Default value, if provided.
+4. Empty string.
+
+With `load_dotenv(override=False)` (the default), the value of a variable is
+the first of the values defined in the following list:
+
+1. Value of that variable in the environment.
+2. Value of that variable in the `.env` file.
+3. Default value, if provided.
+4. Empty string.
+
+!!! note
+
+ `dotenv_values()` does not have an `override` parameter and always resolves
+ variables using the `override=True` order above.
diff --git a/docs/reference/index.md b/docs/reference/index.md
new file mode 100644
index 00000000..ffb9b027
--- /dev/null
+++ b/docs/reference/index.md
@@ -0,0 +1,7 @@
+# Reference
+
+Technical reference for python-dotenv.
+
+- [**Python API**](api.md): All public functions with their signatures and parameters.
+- [**CLI Reference**](cli.md): Complete command-line interface with all options.
+- [**File Format**](file-format.md): Specification of the `.env` file syntax.
diff --git a/mkdocs.yml b/mkdocs.yml
index 3d55d899..c23ccaa1 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,30 +1,79 @@
site_name: python-dotenv
repo_url: https://github.com/theskumar/python-dotenv
-edit_uri: ""
theme:
- name: material
- palette:
- primary: green
features:
+ - content.action.edit # Edit this page
+ - content.action.view # View source of this pageheme:
+ - content.code.copy
- toc.follow
- navigation.sections
+ - navigation.top
+ - navigation.indexes
+ - navigation.footer
+ palette:
+ # Palette toggle for automatic mode
+ - media: "(prefers-color-scheme)"
+ toggle:
+ icon: lucide/sun-moon
+ name: Switch to light mode
+
+ # Palette toggle for light mode
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ toggle:
+ icon: lucide/sun
+ name: Switch to dark mode
+
+ # Palette toggle for dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ toggle:
+ icon: lucide/moon
+ name: Switch to system preference
markdown_extensions:
+ - admonition
+ - md_in_html
+ - pymdownx.highlight:
+ anchor_linenums: true
+ line_spans: __span
+ pygments_lang_class: true
+ - pymdownx.inlinehilite
+ - pymdownx.tabbed:
+ alternate_style: true
+ - pymdownx.superfences
- mdx_truly_sane_lists
+ - pymdownx.snippets:
+ base_path: ["./"] # Folder(s) where snippets are stored
+ check_paths: true # Fail build if a snippet is missing
+ encoding: "utf-8" # Default encoding for reading files
plugins:
- mkdocstrings:
handlers:
python:
options:
+ docstring_style: google
separate_signature: true
show_root_heading: true
show_symbol_type_heading: true
show_symbol_type_toc: true
- search
+
nav:
- Home: index.md
+ - Getting Started: getting-started.md
+ - How-to Guides:
+ - guides/index.md
+ - Configuration Patterns: guides/configuration.md
+ - Command line: guides/cli.md
+ - IPython & Jupyter: guides/ipython.md
+ - Advanced Usage: guides/advanced.md
+ - Reference:
+ - reference/index.md
+ - Python API: reference/api.md
+ - CLI Reference: reference/cli.md
+ - File Format: reference/file-format.md
- Changelog: changelog.md
- Contributing: contributing.md
- - Reference: reference.md
- License: license.md
diff --git a/requirements-docs.txt b/requirements-docs.txt
index b09a710d..b91060d8 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -1,5 +1,3 @@
mdx_truly_sane_lists>=1.3
-mkdocs-include-markdown-plugin>=6.0.0
-mkdocs-material>=9.5.0
mkdocstrings[python]>=0.24.0
-mkdocs>=1.5.0
+zensical
diff --git a/src/dotenv/__init__.py b/src/dotenv/__init__.py
index dde24a01..714fddca 100644
--- a/src/dotenv/__init__.py
+++ b/src/dotenv/__init__.py
@@ -4,6 +4,7 @@
def load_ipython_extension(ipython: Any) -> None:
+ """Register the ``%dotenv`` magic command in an IPython session."""
from .ipython import load_ipython_extension
load_ipython_extension(ipython)
@@ -16,10 +17,10 @@ def get_cli_string(
value: Optional[str] = None,
quote: Optional[str] = None,
):
- """Returns a string suitable for running as a shell script.
+ """Build a shell command string for invoking the ``dotenv`` CLI.
- Useful for converting a arguments passed to a fabric task
- to be passed to a `local` or `run` command.
+ Useful for converting arguments passed to a Fabric task into a string
+ suitable for ``local`` or ``run``.
"""
command = ["dotenv"]
if quote:
diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py
index 79613e28..6a859ef5 100644
--- a/src/dotenv/cli.py
+++ b/src/dotenv/cli.py
@@ -22,10 +22,9 @@
def enumerate_env() -> Optional[str]:
- """
- Return a path for the ${pwd}/.env file.
+ """Return the path to a ``.env`` file in the current working directory.
- If pwd does not exist, return None.
+ Return ``None`` if the working directory no longer exists.
"""
try:
cwd = os.getcwd()
@@ -202,23 +201,10 @@ def run(ctx: click.Context, override: bool, commandline: tuple[str, ...]) -> Non
def run_command(command: List[str], env: Dict[str, str]) -> None:
- """Replace the current process with the specified command.
-
- Replaces the current process with the specified command and the variables from `env`
- added in the current environment variables.
-
- Parameters
- ----------
- command: List[str]
- The command and it's parameters
- env: Dict
- The additional environment variables
-
- Returns
- -------
- None
- This function does not return any value. It replaces the current process with the new one.
+ """Replace the current process with *command*, merging *env* into the environment.
+ On Windows, spawns a subprocess and waits for it instead, because
+ ``execvpe`` returns immediately on that platform.
"""
# copy the current environment variables and add the vales from
# `env`
diff --git a/src/dotenv/ipython.py b/src/dotenv/ipython.py
index 4e7edbbf..274e08b4 100644
--- a/src/dotenv/ipython.py
+++ b/src/dotenv/ipython.py
@@ -10,6 +10,8 @@
@magics_class
class IPythonDotEnv(Magics):
+ """IPython magics for loading ``.env`` files via the ``%dotenv`` command."""
+
@magic_arguments()
@argument(
"-o",
@@ -32,6 +34,7 @@ class IPythonDotEnv(Magics):
)
@line_magic
def dotenv(self, line):
+ """Load a ``.env`` file into the IPython session's environment."""
args = parse_argstring(self.dotenv, line)
# Locate the .env file
dotenv_path = args.dotenv_path
diff --git a/src/dotenv/main.py b/src/dotenv/main.py
index 491634d9..e9feb353 100644
--- a/src/dotenv/main.py
+++ b/src/dotenv/main.py
@@ -20,9 +20,7 @@
def _load_dotenv_disabled() -> bool:
- """
- Determine if dotenv loading has been disabled.
- """
+ """Return ``True`` if the ``PYTHON_DOTENV_DISABLED`` env var is set to a truthy value."""
if "PYTHON_DOTENV_DISABLED" not in os.environ:
return False
value = os.environ["PYTHON_DOTENV_DISABLED"].casefold()
@@ -30,6 +28,7 @@ def _load_dotenv_disabled() -> bool:
def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding]:
+ """Yield each binding, logging a warning for any that failed to parse."""
for mapping in mappings:
if mapping.error:
logger.warning(
@@ -40,6 +39,21 @@ def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding
class DotEnv:
+ """Parse a ``.env`` file and expose its key-value pairs.
+
+ Provide the content either as a filesystem path via *dotenv_path* or as an
+ already-opened text stream via *stream*. Variable interpolation
+ (``${VAR}`` syntax) is performed by default and can be disabled with
+ *interpolate*.
+
+ Attributes:
+ dotenv_path: Path to the ``.env`` file, or ``None`` when using a stream.
+ encoding: Encoding used to open the file.
+ interpolate: Whether ``${VAR}`` references in values are resolved.
+ override: Whether dotenv values take precedence over existing
+ environment variables during interpolation.
+ """
+
def __init__(
self,
dotenv_path: Optional[StrPath],
@@ -73,7 +87,16 @@ def _get_stream(self) -> Iterator[IO[str]]:
yield io.StringIO("")
def dict(self) -> Dict[str, Optional[str]]:
- """Return dotenv as dict"""
+ """Return the parsed .env content as an ordered dictionary.
+
+ Results are cached after the first call. If interpolation is enabled,
+ variable references (e.g. ``${VAR}``) within values are resolved
+ against previously parsed dotenv values and the system environment.
+
+ Returns:
+ An ordered dict mapping variable names to their values.
+ Keys declared without a value will have ``None`` as their value.
+ """
if self._dict:
return self._dict
@@ -89,14 +112,17 @@ def dict(self) -> Dict[str, Optional[str]]:
return self._dict
def parse(self) -> Iterator[Tuple[str, Optional[str]]]:
+ """Yield ``(key, value)`` pairs parsed from the ``.env`` source."""
with self._get_stream() as stream:
for mapping in with_warn_for_invalid_lines(parse_stream(stream)):
if mapping.key is not None:
yield mapping.key, mapping.value
def set_as_environment_variables(self) -> bool:
- """
- Load the current dotenv as system environment variable.
+ """Set all parsed key-value pairs as environment variables.
+
+ Returns:
+ ``True`` if at least one variable was present, ``False`` otherwise.
"""
if not self.dict():
return False
@@ -110,7 +136,7 @@ def set_as_environment_variables(self) -> bool:
return True
def get(self, key: str) -> Optional[str]:
- """ """
+ """Return the value for *key*, or ``None`` if it is not present."""
data = self.dict()
if key in data:
@@ -127,10 +153,9 @@ def get_key(
key_to_get: str,
encoding: Optional[str] = "utf-8",
) -> Optional[str]:
- """
- Get the value of a given key from the given .env.
+ """Return the value of *key_to_get* from the ``.env`` file at *dotenv_path*.
- Returns `None` if the key isn't found or doesn't have a value.
+ Return ``None`` if the key is absent or declared without a value.
"""
return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get)
@@ -141,6 +166,25 @@ def rewrite(
encoding: Optional[str],
follow_symlinks: bool = False,
) -> Iterator[Tuple[IO[str], IO[str]]]:
+ """Context manager for atomically rewriting a file.
+
+ Yields a ``(source, dest)`` pair of text streams. ``source`` is the
+ existing file opened for reading (or an empty ``StringIO`` if the file
+ doesn't exist yet). ``dest`` is a temporary file in the same directory
+ opened for writing. The caller should read from ``source`` and write the
+ desired new content to ``dest``.
+
+ On a clean exit the temporary file is moved into place via
+ ``os.replace()``, preserving the original file's permission bits. If an
+ exception occurs, the temporary file is removed and the original is left
+ untouched.
+
+ Parameters:
+ path: Path to the file to rewrite (created if it doesn't exist).
+ encoding: Encoding used to open both the source and destination files.
+ follow_symlinks: If ``True``, resolve symlinks so the real file is
+ rewritten rather than replacing the symlink itself.
+ """
if follow_symlinks:
path = os.path.realpath(path)
@@ -199,14 +243,18 @@ def set_key(
encoding: Optional[str] = "utf-8",
follow_symlinks: bool = False,
) -> Tuple[Optional[bool], str, str]:
- """
- Adds or Updates a key/value to the given .env
+ """Add or update a key-value pair in the ``.env`` file at *dotenv_path*.
- The target .env file is created if it doesn't exist.
+ Create the file if it does not already exist. Symlinks are **not**
+ followed by default to avoid writing to an untrusted target; set
+ *follow_symlinks* to ``True`` to override this.
+
+ Returns:
+ A ``(True, key, value)`` tuple on success.
- This function doesn't follow symlinks by default, to avoid accidentally
- modifying a file at a potentially untrusted path. If you don't need this
- protection and need symlinks to be followed, use `follow_symlinks`.
+ Raises:
+ ValueError: If *quote_mode* is not one of ``"always"``,
+ ``"auto"``, or ``"never"``.
"""
if quote_mode not in ("always", "auto", "never"):
raise ValueError(f"Unknown quote_mode: {quote_mode}")
@@ -252,15 +300,14 @@ def unset_key(
encoding: Optional[str] = "utf-8",
follow_symlinks: bool = False,
) -> Tuple[Optional[bool], str]:
- """
- Removes a given key from the given `.env` file.
+ """Remove *key_to_unset* from the ``.env`` file at *dotenv_path*.
- If the .env path given doesn't exist, fails.
- If the given key doesn't exist in the .env, fails.
+ Return ``(None, key)`` and log a warning if the file does not exist or the
+ key is not found. Symlinks are **not** followed by default; set
+ *follow_symlinks* to ``True`` to override this.
- This function doesn't follow symlinks by default, to avoid accidentally
- modifying a file at a potentially untrusted path. If you don't need this
- protection and need symlinks to be followed, use `follow_symlinks`.
+ Returns:
+ ``(True, key)`` on success, ``(None, key)`` on failure.
"""
if not os.path.exists(dotenv_path):
logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path)
@@ -290,6 +337,26 @@ def resolve_variables(
values: Iterable[Tuple[str, Optional[str]]],
override: bool,
) -> Mapping[str, Optional[str]]:
+ """Resolve variable interpolations in a sequence of key-value pairs.
+
+ Replace variable references (e.g. ``${VAR}`` or ``$VAR``) within values by
+ looking them up in the already-resolved dotenv values and the system
+ environment. The *override* flag controls precedence: when ``True``, dotenv
+ values take priority over existing environment variables; when ``False``,
+ existing environment variables take priority over dotenv values.
+
+ Parameters:
+ values: An iterable of ``(key, value)`` pairs as produced by
+ :meth:`DotEnv.parse`. Values may be ``None`` for keys that were
+ declared without an assignment.
+ override: If ``True``, previously parsed dotenv values override system
+ environment variables during interpolation. If ``False``, system
+ environment variables take precedence.
+
+ Returns:
+ An ordered mapping of resolved key-value pairs, preserving the
+ original iteration order.
+ """
new_values: Dict[str, Optional[str]] = {}
for name, value in values:
@@ -299,11 +366,11 @@ def resolve_variables(
atoms = parse_variables(value)
env: Dict[str, Optional[str]] = {}
if override:
- env.update(os.environ) # type: ignore
+ env.update(os.environ)
env.update(new_values)
else:
env.update(new_values)
- env.update(os.environ) # type: ignore
+ env.update(os.environ)
result = "".join(atom.resolve(env) for atom in atoms)
new_values[name] = result
@@ -312,9 +379,7 @@ def resolve_variables(
def _walk_to_root(path: str) -> Iterator[str]:
- """
- Yield directories starting from the given directory up to the root
- """
+ """Yield directories starting from *path* up to the filesystem root."""
if not os.path.exists(path):
raise IOError("Starting path not found")
@@ -334,10 +399,9 @@ def find_dotenv(
raise_error_if_not_found: bool = False,
usecwd: bool = False,
) -> str:
- """
- Search in increasingly higher folders for the given file
+ """Search for a ``.env`` file by walking up from the caller's directory.
- Returns path to the file if found, or an empty string otherwise
+ Return the absolute path if found, or an empty string otherwise.
"""
def _is_interactive():
@@ -399,6 +463,7 @@ def load_dotenv(
from the `.env` file.
interpolate: Whether to interpolate variables using POSIX variable expansion.
encoding: Encoding to be used to read the file.
+
Returns:
Bool: True if at least one environment variable is set else False
@@ -437,22 +502,20 @@ def dotenv_values(
interpolate: bool = True,
encoding: Optional[str] = "utf-8",
) -> Dict[str, Optional[str]]:
- """
- Parse a .env file and return its content as a dict.
+ """Parse a ``.env`` file and return its content as a dict.
- The returned dict will have `None` values for keys without values in the .env file.
- For example, `foo=bar` results in `{"foo": "bar"}` whereas `foo` alone results in
- `{"foo": None}`
+ Keys declared without a value (e.g. bare ``FOO``) will have ``None`` as
+ their dict value.
Parameters:
dotenv_path: Absolute or relative path to the .env file.
stream: `StringIO` object with .env content, used if `dotenv_path` is `None`.
verbose: Whether to output a warning if the .env file is missing.
- interpolate: Whether to interpolate variables using POSIX variable expansion.
+ interpolate: Whether ``${VAR}`` references in values are resolved.
encoding: Encoding to be used to read the file.
- If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
- .env file.
+ If both *dotenv_path* and *stream* are ``None``, :func:`find_dotenv` is
+ used to locate the ``.env`` file.
"""
if dotenv_path is None and stream is None:
dotenv_path = find_dotenv()
@@ -468,9 +531,7 @@ def dotenv_values(
def _is_file_or_fifo(path: StrPath) -> bool:
- """
- Return True if `path` exists and is either a regular file or a FIFO.
- """
+ """Return ``True`` if *path* exists and is a regular file or a FIFO."""
if os.path.isfile(path):
return True
diff --git a/src/dotenv/parser.py b/src/dotenv/parser.py
index eb100b47..471132fe 100644
--- a/src/dotenv/parser.py
+++ b/src/dotenv/parser.py
@@ -33,11 +33,24 @@ def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]:
class Original(NamedTuple):
+ """Raw text of a parsed line and its 1-based line number."""
+
string: str
line: int
class Binding(NamedTuple):
+ """Parsed result for a single line of a ``.env`` file.
+
+ Attributes:
+ key: Variable name, or ``None`` for comments, blank lines, and
+ unparseable lines.
+ value: Assigned value, or ``None`` when the key has no ``=`` sign
+ or the line could not be parsed.
+ original: The raw text and line number this binding was parsed from.
+ error: ``True`` if the line could not be parsed.
+ """
+
key: Optional[str]
value: Optional[str]
original: Original
@@ -67,6 +80,8 @@ class Error(Exception):
class Reader:
+ """Stateful reader that consumes a ``.env`` stream character by character."""
+
def __init__(self, stream: IO[str]) -> None:
self.string = stream.read()
self.position = Position.start()
@@ -103,6 +118,8 @@ def read_regex(self, regex: Pattern[str]) -> Sequence[str]:
def decode_escapes(regex: Pattern[str], string: str) -> str:
+ """Replace escape sequences matched by *regex* with their decoded characters."""
+
def decode_match(match: Match[str]) -> str:
return codecs.decode(match.group(0), "unicode-escape") # type: ignore
@@ -110,6 +127,7 @@ def decode_match(match: Match[str]) -> str:
def parse_key(reader: Reader) -> Optional[str]:
+ """Parse and return the key portion of a binding, or ``None`` for comments."""
char = reader.peek(1)
if char == "#":
return None
@@ -121,11 +139,13 @@ def parse_key(reader: Reader) -> Optional[str]:
def parse_unquoted_value(reader: Reader) -> str:
+ """Parse an unquoted value, stripping inline comments and trailing whitespace."""
(part,) = reader.read_regex(_unquoted_value)
return re.sub(r"\s+#.*", "", part).rstrip()
def parse_value(reader: Reader) -> str:
+ """Parse a value that may be single-quoted, double-quoted, or unquoted."""
char = reader.peek(1)
if char == "'":
(value,) = reader.read_regex(_single_quoted_value)
@@ -140,6 +160,7 @@ def parse_value(reader: Reader) -> str:
def parse_binding(reader: Reader) -> Binding:
+ """Parse the next complete binding (key-value pair, comment, or blank line)."""
reader.set_mark()
try:
reader.read_regex(_multiline_whitespace)
@@ -177,6 +198,7 @@ def parse_binding(reader: Reader) -> Binding:
def parse_stream(stream: IO[str]) -> Iterator[Binding]:
+ """Yield :class:`Binding` instances for every line in a ``.env`` stream."""
reader = Reader(stream)
while reader.has_next():
yield parse_binding(reader)
diff --git a/src/dotenv/variables.py b/src/dotenv/variables.py
index 667f2f26..cb9f49ac 100644
--- a/src/dotenv/variables.py
+++ b/src/dotenv/variables.py
@@ -16,6 +16,8 @@
class Atom(metaclass=ABCMeta):
+ """Base class for the components of a parsed variable-interpolation string."""
+
def __ne__(self, other: object) -> bool:
result = self.__eq__(other)
if result is NotImplemented:
@@ -23,10 +25,14 @@ def __ne__(self, other: object) -> bool:
return not result
@abstractmethod
- def resolve(self, env: Mapping[str, Optional[str]]) -> str: ...
+ def resolve(self, env: Mapping[str, Optional[str]]) -> str:
+ """Return the resolved string value using *env* for variable lookups."""
+ ...
class Literal(Atom):
+ """Plain text segment that contains no variable references."""
+
def __init__(self, value: str) -> None:
self.value = value
@@ -46,6 +52,8 @@ def resolve(self, env: Mapping[str, Optional[str]]) -> str:
class Variable(Atom):
+ """Reference to a variable (``${name}`` or ``${name:-default}``)."""
+
def __init__(self, name: str, default: Optional[str]) -> None:
self.name = name
self.default = default
@@ -68,6 +76,7 @@ def resolve(self, env: Mapping[str, Optional[str]]) -> str:
def parse_variables(value: str) -> Iterator[Atom]:
+ """Parse *value* into a sequence of :class:`Literal` and :class:`Variable` atoms."""
cursor = 0
for match in _posix_variable.finditer(value):