Quillstack Dotenv
Reads a .env file. Values keep the type they plainly have, and nothing is expanded unless you
ask for it.
Why this exists
It does not expand ${SOMETHING}, and that is on purpose
Most .env libraries resolve one value from another out of the box. This one does not, and the
model it follows is JavaScript's.
dotenv for Node is the most installed .env library anywhere, and it does not interpolate.
Nor does dotenv-java. In that world, building values from other values is a second package,
because it is a second decision: it turns a list of pairs into a small language, with escaping
and ordering and undefined names to settle.
The same choice is made here, and by
quillstack/dotenv in PHP. A .env file which quietly
became a template is a .env file you have to read twice.
A value keeps the type it has
if settings["APP_DEBUG"]:
...
Read as text, false is a non-empty string and therefore true. That is a bug which looks like
working code, and it is the reason the values are typed rather than handed back as strings.
The values do not go into os.environ
os.environ holds strings and nothing else, so a port put there comes back '5432' and a
false comes back true — the exact thing the typing avoids. What is read stays where its types
survive, and export() is there for when something else needs the environment set.
Requirements
- Python 3.11 or newer
Installation
pip install quillstack-dotenv
Usage
APP_DEBUG=true
APP_NAME=quillstack
DB_PORT=5432
from quillstack.dotenv import Dotenv
settings = Dotenv(".env").load()
settings["APP_DEBUG"] # True, a boolean
settings["APP_NAME"] # 'quillstack'
settings["DB_PORT"] # 5432, a number
It is a Mapping, so len(), in, iteration and unpacking all work on it.
Saying you meant the text
Quote it:
DB_PORT_TEXT="5432"
settings["DB_PORT_TEXT"] # '5432', a string
Default values
settings.get("MISSING", "a default") # 'a default'
settings.get("MISSING") # None
Required keys
Where there is no sensible default, say so and find out at boot rather than at midnight:
settings.required("DATABASE_HOST")
ValueNotSetError: Value not set for key: DATABASE_HOST
Comments after a value
A # starts a comment where a shell would treat it as one — after whitespace, and outside
quotes:
COMMENTED=5432 # the default
PASSWORD=hunter2#7
QUOTED="a # inside quotes"
settings["COMMENTED"] # 5432
settings["PASSWORD"] # 'hunter2#7'
settings["QUOTED"] # 'a # inside quotes'
A parser which took every # would turn a password into a shorter password and say nothing
about it.
Nothing is expanded
URL=https://${APP_NAME}.org
settings["URL"] # 'https://${APP_NAME}.org'
Setting the environment anyway
For the sake of something else which reads os.environ directly:
settings.export() # leaves what is already set alone
settings.export(override=True) # does not
What goes out is text: True is exported as true, because that is what it was in the file and
what another reader expects to find. A deployment which set a variable meant it, which is why
what is already there wins unless you say otherwise.
Benchmark
Against python-dotenv 1.2.3, on Python 3.13.5, reading the same 23-pair file. Interleaved
runs, median of five.
Read what each one does before reading the numbers, because they are not doing the same work — in both directions:
quillstack-dotenv |
python-dotenv |
|
|---|---|---|
A=true |
True |
'true' |
B=5432 |
5432 |
'5432' |
C=${A} |
'${A}' |
'true' — expanded |
| multi-line values | no | yes |
${MISSING:-fallback} |
no | yes |
| escapes inside quotes | no | yes |
So one of them reads types and the other builds a small language. Now the numbers, measured with quillstack-benchmark — interleaved runs, the median of five, versions read from what is installed:
| Version | |
|---|---|
| python-dotenv | 1.2.3 |
| quillstack-dotenv | 0.1.1 |
| Per call | Relative | |
|---|---|---|
| quillstack-dotenv | 31.3 µs | — |
| python-dotenv | 446.9 µs | 14.3× |
That is not the number that matters, because a .env file is read once. What an application
pays is the whole thing — importing the library and loading the file:
| Whole process | Of which is the library | |
|---|---|---|
| the interpreter, importing nothing | 15.75 ms | — |
| quillstack-dotenv | 20.25 ms | 4.50 ms |
| python-dotenv | 28.77 ms | 13.02 ms |
About three quarters of that is Python starting, and no .env library can do anything about it.
Choosing between these two moves a boot by eight milliseconds.
Being faster because you do less is not being faster. If you want multi-line values, defaults
inside ${…}, or escapes, python-dotenv has them and this does not — and it is the more
popular library in this ecosystem by a wide margin, which is worth knowing when you pick.
Tests
uv run pytest
Static analysis
uv run ruff check --no-cache
uv run mypy
--no-cache on purpose: a cached ruff result once said a Quillstack package was clean while CI
said it was not.
Against the standard
uv run quillstack-standards .
The rest of Quillstack
This is one component of Quillstack, the same way of building APIs in more than one language.
- quillstack-standards — what keeps every package the same shape
- quillstack/dotenv — the same decisions in PHP
- quillstack/dotenv-expand — values built from other values, where you want them
License
MIT — see LICENSE.
Metadata
Release files for quillstack-dotenv 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| quillstack_dotenv-0.1.2.tar.gz | 53.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| quillstack_dotenv-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 63.4 kB
Release files / quillstack_dotenv-0.1.2.tar.gz
| Download URL | quillstack_dotenv-0.1.2.tar.gz |
|---|---|
| Size | 53.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
96355eec8f98dae5674a6be349a6fd1ee702b4b0e9627b01735949397c55bf13
|
|
BLAKE2b-256 checksum How to use checksums |
bb198f5f8ca7b88c1dd80454f21e3a038a690bb8f17d08e11a304ca60290ebbc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency logRelease files / quillstack_dotenv-0.1.2-py3-none-any.whl
| Download URL | quillstack_dotenv-0.1.2-py3-none-any.whl |
|---|---|
| Size | 9.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
46f5841439a0bc878a42b5ea8d9e8c7d13c3803647ad89b04b41674bb27d3895
|
|
BLAKE2b-256 checksum How to use checksums |
d912b0dd639ed86a3226ab4f2df2270203b9d8abbef7e8e421c9137fb6950791
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency log