tidyenv
Typed environment variables with friendly errors. Built-in .env support. Zero dependencies.
from tidyenv import env
PORT = env.int("PORT", default=8000)
DEBUG = env.bool("DEBUG", default=False)
HOSTS = env.list("ALLOWED_HOSTS", default=["localhost"])
DATABASE_URL = env.str("DATABASE_URL")
Each line converts the type, applies the default, and if something is wrong, says exactly what:
tidyenv.EnvError: 1 environment problem:
- PORT: expected an integer (got 'eighty')
Why
Plain os.environ |
tidyenv | |
|---|---|---|
| Number with a default | int(os.environ.get("PORT", "8000")) |
env.int("PORT", default=8000) |
| Bad number | ValueError: invalid literal for int() with base 10: 'eighty' (which variable?) |
PORT: expected an integer (got 'eighty') |
| Missing variable | KeyError: 'DB_URL' |
DB_URL: is not set |
Empty value DB_URL= |
silently "" |
treated as not set |
| Boolean | os.environ.get("DEBUG", "").lower() in ("1", "true", "yes"), and a typo like ture silently means False |
env.bool("DEBUG", default=False), and ture is an error |
| List | [h.strip() for h in os.environ.get("HOSTS", "").split(",") if h.strip()] |
env.list("HOSTS") |
| List of numbers | the same, plus int() on every item |
env.list("PORTS", of=int) |
| One of several values | if mode not in ("dev", "prod"): raise ... |
env.choice("MODE", ["dev", "prod"]) |
| Secrets in errors | the value ends up in your logs | secret=True shows *** |
.env file |
pip install python-dotenv + load_dotenv() |
the same load_dotenv(), built in |
| Types for mypy / IDE | str | None, cast it yourself |
env.int returns int |
| Several broken variables | crash, fix, redeploy, crash on the next one | env.collect() lists them all at once |
One line per variable, and every error names the variable and says what is wrong with it.
Install
pip install tidyenv
Python 3.9+.
Usage
| Reader | Example value | Returns |
|---|---|---|
env.str(name, allow_empty=False) |
hello |
str (stripped) |
env.int(name) |
8000, 1_000 |
int |
env.float(name) |
0.25 |
float (nan and inf are rejected) |
env.bool(name) |
true/false, yes/no, on/off, 1/0, any case |
bool |
env.list(name, sep=",", of=str) |
a, b, c |
list (use of=int to convert items; int, float and bool follow the rules above) |
env.choice(name, choices) |
prod, PROD |
the matching entry of choices, any case |
env.path(name, must_exist=False) |
~/data |
pathlib.Path with ~ expanded |
env.json(name) |
{"a": 1} |
parsed JSON |
Every reader takes an optional default. Without one, a missing or empty variable is
an error. With one, the default is returned instead, and type checkers know the result
is int | <type of default>.
An empty value like DB_URL= usually means someone forgot to fill it in, so it counts
as missing: you get the default, or the error DB_URL: is empty. This is how the shell's
${DB_URL:-default} treats it too. When empty really is a valid value, such as a local
database with no password, say so: env.str("DB_PASSWORD", allow_empty=True) returns "".
Switching from os.environ
tidyenv is stricter than hand-written parsing, on purpose. When you switch, check the values your environments actually set:
- Bad values stop the app. A typo in a number, a boolean or a choice is an error at
startup instead of a silent fallback.
env.choiceignores case, soPRODis fine. - Booleans accept every common spelling.
true,True,yes,onand1are allTrue. Code that only checked== "TRUE"treated the others asFalse. - Empty means missing.
X=gives the default (or anis emptyerror), not"", unless you passallow_empty=True. If your old code usedos.getenv("X", "")and relied on getting"", useallow_empty=Truefor that variable.
.env files
Coming from python-dotenv? Change the import, nothing else:
from tidyenv import load_dotenv # was: from dotenv import load_dotenv
load_dotenv()
load_dotenv, dotenv_values and find_dotenv take the same arguments and give the
same results as python-dotenv 1.2's:
- the same file syntax:
export, comments, quotes, values spanning several lines,${VAR}and${VAR:-default}; .envis looked up starting next to the calling file, then in its parents;- values go into
os.environ, and variables that are already set are kept unlessoverride=True, so the first file you load wins; PYTHON_DOTENV_DISABLED=1turns loading off.
The test suite checks this against python-dotenv itself on thousands of generated
files. Not included: set_key, unset_key, get_key and the dotenv command.
Then read the values with types. env reads os.environ, so it sees them:
from tidyenv import env, load_dotenv
load_dotenv()
PORT = env.int("PORT", default=8000)
Without touching os.environ. env.read_dotenv() follows the same rules but keeps
the values inside env, and a line it can't parse is an error naming the file and line
(python-dotenv only logs a warning):
env.read_dotenv() # ./.env in the current directory, if it exists
env.read_dotenv("config/dev.env", required=True)
Need just the parser? tidyenv.parse_dotenv(text) returns a dict (without ${VAR}
expansion).
All errors at once (optional)
By default the first bad variable raises EnvError. If you'd rather see every
problem in one go, wrap your reads in env.collect():
with env.collect():
PORT = env.int("PORT", default=8000)
DATABASE_URL = env.str("DATABASE_URL")
API_KEY = env.str("API_KEY", secret=True)
tidyenv.EnvError: 3 environment problems:
- PORT: expected an integer (got 'eighty')
- DATABASE_URL: is not set
- API_KEY: is not set
Failed reads return None inside the block, so only use the values after it exits.
More
Secrets. Pass secret=True and the raw value is shown as *** in error messages,
including errors about single env.list items.
Prefixes and custom sources. Build your own reader:
from tidyenv import Env
env = Env(prefix="MYAPP_") # reads MYAPP_PORT for env.int("PORT")
test_env = Env(environ={"PORT": "1"}) # any mapping, handy in tests
Handling errors. EnvError.problems is a list of Problem(name, message), so you
can print them your own way.
License
MIT. The .env support is adapted from
python-dotenv (BSD-3-Clause, see
LICENSES/python-dotenv.txt); thanks to its authors.
tidyenv is not affiliated with or endorsed by the python-dotenv project.
Metadata
Release files for tidyenv 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tidyenv-0.3.0.tar.gz | 84.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tidyenv-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 100.5 kB
Release files / tidyenv-0.3.0.tar.gz
| Download URL | tidyenv-0.3.0.tar.gz |
|---|---|
| Size | 84.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4e6371a46023daebb0e6fe64dd1b1bdbc92daaab77a6f9a1e325adfaa3b18dfb
|
|
BLAKE2b-256 checksum How to use checksums |
f228829c143d790e066e9786d4038ccce62dd70af916992692873e8832e93134
|
| 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 Oct 3, 2026.
Transparency logRelease files / tidyenv-0.3.0-py3-none-any.whl
| Download URL | tidyenv-0.3.0-py3-none-any.whl |
|---|---|
| Size | 16.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
615af21731ffd7a3d58d0df0d971be3645291f09dd5f279f46f8098d26214590
|
|
BLAKE2b-256 checksum How to use checksums |
a1785d5e6485c92a0e68897a152b475a232b7f1931b667dc691219f830c947a9
|
| 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 Oct 3, 2026.
Transparency log