Typed, dependency-free environment variable loading: read env vars as int/bool/list/json/Path with defaults, required-checks, and clear errors.
Project description
envcaster ⚙️
Read environment variables as the type you actually want —
int,bool,list,json,Path— with defaults, required-checks, and errors that name the offending variable. Zero dependencies, pure standard library.
os.environ only ever gives you strings. So every project grows the same little
pile of int(os.environ.get("PORT", "8000")) and hand-rolled truthy checks that
quietly treat "False" as True. envcaster is that pile, done once and done right.
- 🪶 Zero required dependencies — pure standard library.
- 🎯 Typed getters —
str · int · float · bool · list · json · path(+ customcast). - ✅ Built-in validation —
choices,min/maxbounds, and aValidationErrorthat's distinct from parse failures. - 📋 Batch config checks —
env.collect()reports every bad/missing variable at once, not one per restart. - 🧯 Loud, precise errors — missing or malformed values tell you which variable and why.
- 🧪 Fully tested across Python 3.9–3.12.
- 🧩 Drop-in
.envloader with optional${VAR}interpolation — never clobbers real environment config.
Install
pip install envcaster
Quick start
from envcaster import env
PORT = env.int("PORT", default=8000)
DEBUG = env.bool("DEBUG", default=False)
HOSTS = env.list("ALLOWED_HOSTS", sep=",") # ["a", "b"] from "a,b"
TIMEOUT = env.float("TIMEOUT", default=1.5)
SECRET = env.str("SECRET_KEY", required=True) # raises if not set
DATA = env.path("DATA_DIR", default="/var/data") # -> pathlib.Path
FLAGS = env.json("FEATURE_FLAGS", default={}) # parsed JSON
A variable is required unless you give it a
default. Missing required variables raiseMissingEnvError; bad values raiseCastError. Both subclassEnvError— catch broadly or narrowly.
Usage
Booleans that actually behave
env.bool("DEBUG") # 1 true t yes y on -> True (case-insensitive)
# 0 false f no n off -> False
# anything else -> CastError
No more bool("False") == True bugs.
Lists (and lists of other types)
env.list("ALLOWED_HOSTS") # "a, b ,, c" -> ["a", "b", "c"] (trims, drops empties)
env.list("PORTS", sep=":", cast=int) # "80:443" -> [80, 443]
env.list("TAGS", default=[]) # missing -> []
JSON and paths
env.json("LIMITS") # '{"rpm": 60}' -> {"rpm": 60}
env.path("LOG_DIR") # "/var/log" -> PosixPath("/var/log")
Anything else — bring your own cast
from decimal import Decimal
env.cast("PRICE", Decimal) # "9.99" -> Decimal("9.99")
env.cast("COLOR", lambda v: int(v, 16)) # "ff0000" -> 16711680
# exceptions from your function are wrapped in CastError, naming the variable
Validate values — choices, min, max
env.str("STAGE", choices=["dev", "staging", "prod"]) # else ValidationError
env.int("PORT", min=1, max=65535) # range-checked
env.float("SAMPLE_RATE", min=0.0, max=1.0)
env.cast("LEVEL", str.upper, choices=["INFO", "DEBUG"]) # choices check the converted value
A value that parses but breaks a constraint raises ValidationError (a
ValueError), kept distinct from CastError (which means it couldn't be parsed
at all). A default you supply is trusted and never constraint-checked.
Validate a whole config at once — collect()
Stop debugging your config one missing variable per restart. collect() gathers
every error in the block and raises a single report:
from envcaster import env
with env.collect() as cfg:
PORT = cfg.int("PORT", default=8000)
SECRET = cfg.str("SECRET_KEY", required=True)
DB_URL = cfg.str("DATABASE_URL", required=True)
REGION = cfg.str("REGION", choices=["us", "eu"])
# If SECRET_KEY and DATABASE_URL are both missing, you get ONE error:
# EnvValidationError: 2 environment variable errors found:
# - Required environment variable 'SECRET_KEY' is not set.
# - Required environment variable 'DATABASE_URL' is not set.
Scoped readers with a prefix
from envcaster import Env
app = Env(prefix="APP_")
app.int("PORT") # reads APP_PORT
app.bool("DEBUG") # reads APP_DEBUG
Read from somewhere other than os.environ
cfg = Env(source={"PORT": "9000"}) # great for tests — no global state
cfg.int("PORT") # 9000
Load a .env file (no dependency)
from envcaster import load_dotenv, env
load_dotenv() # reads ./.env into os.environ (won't override real env vars)
load_dotenv(".env.local", override=True)
PORT = env.int("PORT")
# Or parse without touching the environment:
from envcaster import read_dotenv
values = read_dotenv(".env") # -> {"PORT": "8000", ...}
# Opt in to ${VAR} / $VAR expansion (from earlier keys, then os.environ):
read_dotenv(".env", interpolate=True) # HOST=localhost / URL=http://${HOST} -> http://localhost
Handles KEY=value, export KEY=value, # comments, and quoted values.
Single-quoted values stay literal and \$ is an escaped dollar. For multiline
values, use python-dotenv.
API reference
| Call | Returns | Notes |
|---|---|---|
env.str(name, default=…, required=False, choices=None) |
str |
The raw value, unchanged |
env.int(name, …, min=None, max=None, choices=None) |
int |
Base-10, whitespace stripped |
env.float(name, …, min=None, max=None, choices=None) |
float |
|
env.bool(name, …) |
bool |
1/true/t/yes/y/on ↔ 0/false/f/no/n/off |
env.list(name, …, sep=",", cast=str) |
list |
Trims items, drops empties, per-item cast |
env.json(name, …) |
Any |
json.loads of the value |
env.path(name, …) |
pathlib.Path |
Not resolved/validated |
env.cast(name, func, …, choices=None) |
Any |
Apply any callable; errors wrapped in CastError |
env.collect() |
context manager | Batch-validate; raises one EnvValidationError |
Env(source=None, prefix="") |
Env |
Custom mapping and/or name prefix |
read_dotenv(path=".env", interpolate=False) |
dict |
Parse a .env file; {} if absent |
load_dotenv(path=".env", override=False, interpolate=False) |
dict |
Inject into os.environ |
Errors: EnvError (base) · MissingEnvError (also KeyError) · CastError (also ValueError) · ValidationError (also ValueError, for choices/min/max) · EnvValidationError (aggregate from collect()).
Why not just os.environ?
# Before
import os
PORT = int(os.environ.get("PORT", "8000"))
DEBUG = os.environ.get("DEBUG", "false").lower() in ("1", "true", "yes")
HOSTS = [h.strip() for h in os.environ.get("ALLOWED_HOSTS", "").split(",") if h.strip()]
# After
from envcaster import env
PORT = env.int("PORT", default=8000)
DEBUG = env.bool("DEBUG", default=False)
HOSTS = env.list("ALLOWED_HOSTS", default=[])
Development
git clone https://github.com/YoungAlpaccino/envcast
cd envcast
pip install -e ".[dev]"
pytest # run tests
ruff check . # lint
License
MIT — see LICENSE. Use it anywhere, including commercially.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file envcaster-0.2.0.tar.gz.
File metadata
- Download URL: envcaster-0.2.0.tar.gz
- Upload date:
- Size: 16.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b26fbf03c2aa7845d19ce24e6054f03ef5eeb92c1cce6cce26cfa92150fdcd24
|
|
| MD5 |
9b2ae7022060eb1d13ac606828b08927
|
|
| BLAKE2b-256 |
5793afd4a24cac96344ea7fd34a34d61a8eea0dc42af3cf7a10286077085b9c5
|
File details
Details for the file envcaster-0.2.0-py3-none-any.whl.
File metadata
- Download URL: envcaster-0.2.0-py3-none-any.whl
- Upload date:
- Size: 11.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e7a46f933616c3c5905a0a12608d9e10e6b8de90a6921b9f81ce054d6b7fbf7a
|
|
| MD5 |
ce32cda0c722068c4ae68f9200f98a01
|
|
| BLAKE2b-256 |
73b2684492ba43c14e93f8ea8c2747061548a70eb5e125edaf2c350b1b71c2e3
|