Skip to main content

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 wantint, bool, list, json, Path — with defaults, required-checks, and errors that name the offending variable. Zero dependencies, pure standard library.

CI PyPI Python License: MIT

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 gettersstr · int · float · bool · list · json · path · decimal · duration · datetime · date · bytes · url (+ custom cast).
  • Built-in validationchoices, min/max bounds, and a ValidationError that's distinct from parse failures.
  • 📋 Batch config checksenv.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 .env loader 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 raise MissingEnvError; bad values raise CastError. Both subclass EnvError — 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")

Richer types — money, durations, dates, bytes, URLs

from decimal import Decimal

env.decimal("PRICE")                       # "9.99"      -> Decimal("9.99")  (exact)
env.duration("TIMEOUT")                    # "1h30m"     -> timedelta(seconds=5400)
env.duration("RETRY", default=None)        # "500ms"     -> timedelta(microseconds=500000)
env.datetime("STARTS_AT")                  # "2026-06-22T12:00:00Z" -> aware datetime (UTC)
env.date("RELEASE_DAY")                    # "2026-06-22" -> date(2026, 6, 22)
env.bytes("API_KEY", encoding="base64")    # "aGVsbG8="  -> b"hello"
env.url("WEBHOOK_URL")                     # requires scheme+host; http/https by default
env.url("REDIS_URL", schemes=["redis"])    # restrict allowed schemes

duration accepts plain seconds or ms s m h d w tokens ("30", "5m", "1h30m", "2d"). decimal/duration/datetime/date all take min/max.

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

db = app.prefixed("DB_")   # chain prefixes
db.str("HOST")             # reads APP_DB_HOST

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/on0/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.decimal(name, …, min=None, max=None, choices=None) Decimal Exact precision
env.duration(name, …, min=None, max=None) timedelta Seconds or 500ms/5m/1h30m/2d/1w
env.datetime(name, …, min=None, max=None) datetime ISO 8601; trailing Z = UTC
env.date(name, …, min=None, max=None) date ISO 8601 YYYY-MM-DD
env.bytes(name, …, encoding="utf-8") bytes Codec, or base64/hex
env.url(name, …, schemes=("http","https")) str Validated; requires scheme + host
env.cast(name, func, …, choices=None) Any Apply any callable; errors wrapped in CastError
env.collect() context manager Batch-validate; raises one EnvValidationError
env.prefixed(prefix) Env New reader with a combined prefix
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

envcaster-0.5.0.tar.gz (23.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

envcaster-0.5.0-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

File details

Details for the file envcaster-0.5.0.tar.gz.

File metadata

  • Download URL: envcaster-0.5.0.tar.gz
  • Upload date:
  • Size: 23.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for envcaster-0.5.0.tar.gz
Algorithm Hash digest
SHA256 a326508f43db0a4212e02a11d54671a278f2dfd482efb3c93dbb893ad4f3980e
MD5 2517dfa12eae7115d1486553c6fa3ff0
BLAKE2b-256 fdbab4d145da7a71553f34b45821b02c93405e01389e1872057b1e7a4ae71dc9

See more details on using hashes here.

File details

Details for the file envcaster-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: envcaster-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 15.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for envcaster-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cc30837a110ebf0c6c485d49793201880899ae30c9b1d4481fe69b7076f38307
MD5 c2c59a58ede275b92c96e1e8e3a00416
BLAKE2b-256 a94503fbfd470788f5430f3d31f398d58ee4d06eade079b91f8eefa13b8e6093

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page