Skip to main content

tidyenv logo

tidyenv

PyPI Python CI License: MIT

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.choice ignores case, so PROD is fine.
  • Booleans accept every common spelling. true, True, yes, on and 1 are all True. Code that only checked == "TRUE" treated the others as False.
  • Empty means missing. X= gives the default (or an is empty error), not "", unless you pass allow_empty=True. If your old code used os.getenv("X", "") and relied on getting "", use allow_empty=True for 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};
  • .env is 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 unless override=True, so the first file you load wins;
  • PYTHON_DOTENV_DISABLED=1 turns 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)

Source distribution for tidyenv 0.3.0
File Size Uploaded
tidyenv-0.3.0.tar.gz 84.2 kB Details

Built distribution (wheel)

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

Release 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

Release history Release notifications | RSS feed

0.3.2

2 release files

0.3.1

2 release files

This release

0.3.0 This release

2 release files

0.2.0

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