Skip to main content

Quillstack Dotenv

Tests Latest Version Downloads Python Version CodeFactor Quality Gate Coverage Maintainability Reliability Security License

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.

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)

Source distribution for quillstack-dotenv 0.1.2
File Size Uploaded
quillstack_dotenv-0.1.2.tar.gz 53.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for quillstack-dotenv 0.1.2
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page