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 (+ 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")

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/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.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

envcaster-0.2.0.tar.gz (16.7 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.2.0-py3-none-any.whl (11.7 kB view details)

Uploaded Python 3

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

Hashes for envcaster-0.2.0.tar.gz
Algorithm Hash digest
SHA256 b26fbf03c2aa7845d19ce24e6054f03ef5eeb92c1cce6cce26cfa92150fdcd24
MD5 9b2ae7022060eb1d13ac606828b08927
BLAKE2b-256 5793afd4a24cac96344ea7fd34a34d61a8eea0dc42af3cf7a10286077085b9c5

See more details on using hashes here.

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

Hashes for envcaster-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e7a46f933616c3c5905a0a12608d9e10e6b8de90a6921b9f81ce054d6b7fbf7a
MD5 ce32cda0c722068c4ae68f9200f98a01
BLAKE2b-256 73b2684492ba43c14e93f8ea8c2747061548a70eb5e125edaf2c350b1b71c2e3

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