Typed environment variables as a frozen dataclass — zero dependencies
Project description
frozenenv
Typed environment variables as a frozen dataclass — zero dependencies.
Install
pip install frozenenv
Note: The PyPI package is
frozenenvbut the import name isenvclass:from envclass import envclass
The problem
Every Python project that reads environment variables ends up with code like this:
import os
# Fragile. Untyped. Scattered across the codebase.
DATABASE_URL = os.environ.get("DATABASE_URL") # str or None — no validation
PORT = int(os.environ.get("PORT", "8000")) # crashes if PORT="abc"
DEBUG = os.environ.get("DEBUG", "false").lower() == "true" # hand-rolled bool parsing
HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",") # no types, no defaults
Problems with this approach:
- No central place listing all required env vars — you have to read all the code
- No type safety —
PORTmight silently stay a string - Crashes deep at runtime, not at startup where it's easy to catch
- No IDE autocomplete on config values
- Every project hand-rolls the same boilerplate differently
The frozenenv way
from envclass import envclass
@envclass
class Config:
DATABASE_URL: str # required — raises EnvError if missing
PORT: int = 8000 # optional, auto-cast to int
DEBUG: bool = False # "true" / "1" / "yes" → True
ALLOWED_HOSTS: list[str] = [] # "a.com,b.com" → ["a.com", "b.com"]
cfg = Config() # reads os.environ + .env file automatically
print(cfg.PORT) # 8000 — actual int, not "8000"
print(cfg.DEBUG) # False — actual bool
print(cfg.ALLOWED_HOSTS)# ["a.com", "b.com"]
cfg.PORT = 9000 # ❌ FrozenInstanceError — config is immutable
Your config class is the single source of truth. Every required variable is visible at a glance, every type is enforced, and the app fails loudly at startup if something is missing — not buried in a runtime error 10 minutes later.
Features
| Feature | frozenenv | python-dotenv | pydantic-settings |
|---|---|---|---|
| Type coercion | ✅ | ❌ | ✅ |
| Frozen / immutable | ✅ | ❌ | ❌ |
| Built-in .env parser | ✅ | ✅ | ✅ |
| Zero dependencies | ✅ | ✅ | ❌ |
| IDE autocomplete | ✅ | ❌ | ✅ |
| list[X] support | ✅ | ❌ | ✅ |
| Optional[X] support | ✅ | ❌ | ✅ |
| Decorator API | ✅ | ❌ | ❌ |
Supported types
| Type | Example env value | Python value |
|---|---|---|
str |
hello |
"hello" |
int |
8000 |
8000 |
float |
3.14 |
3.14 |
bool |
true / 1 / yes |
True |
list[str] |
a.com,b.com |
["a.com", "b.com"] |
list[int] |
80,443,8080 |
[80, 443, 8080] |
str | None |
(missing) | None |
Optional[str] |
(missing) | None |
Bool values
frozenenv accepts a wide range of truthy and falsy string values:
| Truthy | Falsy |
|---|---|
true, True, TRUE |
false, False, FALSE |
1 |
0 |
yes |
no |
on |
off |
enabled |
disabled |
Any other value raises an EnvError with a clear message.
Usage examples
Basic usage
from envclass import envclass
@envclass
class Config:
SECRET_KEY: str
PORT: int = 8000
DEBUG: bool = False
cfg = Config()
print(cfg.SECRET_KEY)
Custom .env file path
@envclass(env_file=".env.production")
class Config:
DATABASE_URL: str
SECRET_KEY: str
Disable .env loading entirely
@envclass(env_file=None)
class Config:
DATABASE_URL: str # reads from os.environ only
.env file overrides real environment variables
@envclass(override=True)
class Config:
PORT: int = 8000 # .env value wins over os.environ
Optional variables
@envclass
class Config:
DATABASE_URL: str
SENTRY_DSN: str | None = None # returns None if not set
REDIS_URL: str | None = None
List of typed values
@envclass
class Config:
ALLOWED_HOSTS: list[str] = [] # "a.com,b.com" → ["a.com","b.com"]
ALLOWED_PORTS: list[int] = [] # "80,443" → [80, 443]
Error handling
import sys
from envclass import envclass, EnvError
@envclass
class Config:
DATABASE_URL: str
SECRET_KEY: str
try:
cfg = Config()
except EnvError as e:
print(f"Configuration error: {e}")
sys.exit(1)
Django example
# settings.py
from envclass import envclass, EnvError
@envclass
class Env:
SECRET_KEY: str
DEBUG: bool = False
DATABASE_URL: str
ALLOWED_HOSTS: list[str] = ["localhost"]
REDIS_URL: str | None = None
env = Env()
SECRET_KEY = env.SECRET_KEY
DEBUG = env.DEBUG
ALLOWED_HOSTS = env.ALLOWED_HOSTS
FastAPI example
# config.py
from functools import lru_cache
from envclass import envclass
@envclass
class Settings:
APP_NAME: str = "My API"
DATABASE_URL: str
SECRET_KEY: str
DEBUG: bool = False
@lru_cache
def get_settings():
return Settings()
# main.py
from fastapi import Depends
from config import Settings, get_settings
@app.get("/info")
def info(settings: Settings = Depends(get_settings)):
return {"app": settings.APP_NAME}
.env file format
frozenenv parses .env files with no external dependency. Supported syntax:
# Comments are ignored
DATABASE_URL=postgres://localhost/mydb
# Quoted values
SECRET_KEY="my secret key with spaces"
APP_NAME='My Application'
# export keyword is supported
export PORT=8000
# Comma-separated lists
ALLOWED_HOSTS=localhost,127.0.0.1,myapp.com
ALLOWED_PORTS=80,443
# Optional values (empty = None for Optional types)
SENTRY_DSN=
API reference
@envclass
Decorator that converts a class into a typed, frozen environment config object.
@envclass(
env_file: str | None = ".env", # path to .env file, None to disable
override: bool = False, # if True, .env values override os.environ
)
Parameters:
env_file— Path to a.envfile. Defaults to.envin the current working directory. Set toNoneto skip file loading entirely.override— IfTrue, values from the.envfile take precedence over real environment variables. Default isFalse(real env wins).
Behaviour:
- All annotated fields without defaults are required — missing ones raise
EnvErrorimmediately - Fields with defaults are optional — the default is used when the variable is not set
- The resulting object is frozen — attempting to set any attribute raises
FrozenInstanceError - Full IDE autocomplete because the result is a real Python dataclass
EnvError
Raised when a required variable is missing or a value cannot be cast to the declared type.
from envclass import EnvError
try:
cfg = Config()
except EnvError as e:
# e.g: "Missing required environment variable(s): DATABASE_URL, SECRET_KEY"
# e.g: "PORT: cannot cast 'abc' to int"
# e.g: "DEBUG: 'maybe' is not a valid bool. Use: 0, 1, disabled, enabled, ..."
print(e)
Requirements
- Python 3.10 or higher
- Zero external dependencies — pure Python stdlib only
Contributing
Contributions are welcome. To get started:
git clone https://github.com/hudihi/frozenenv.git
cd frozenenv
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest tests/ -v
Please open an issue before submitting a pull request for large changes.
Changelog
0.1.0 — 2026-05-12
- Initial release
- Type coercion:
str,int,float,bool,list[X],Optional[X] - Built-in
.envfile parser — no python-dotenv needed - Frozen / immutable config objects
overridemode for.envpriority- Clear
EnvErrormessages for missing and invalid variables
License
Project details
Release history Release notifications | RSS feed
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 frozenenv-0.1.1.tar.gz.
File metadata
- Download URL: frozenenv-0.1.1.tar.gz
- Upload date:
- Size: 7.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b733a42b2f950c3f4023f9f292d44cfbecd1480b10ad1bc1ae9bef61fdf8269
|
|
| MD5 |
c3e4275a18437bc6dcc8188056a63ead
|
|
| BLAKE2b-256 |
55e7c5f062a8672fb63cfae95a1a3b74894148ac2f9a1e771bba96b04b51c06d
|
File details
Details for the file frozenenv-0.1.1-py3-none-any.whl.
File metadata
- Download URL: frozenenv-0.1.1-py3-none-any.whl
- Upload date:
- Size: 8.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ef21ea8fadbfe3767f7cbed117c498e49fa1e56e9340e8186f6bb515d82a0dd4
|
|
| MD5 |
ac6ff06a0763220dcced94aa8b4a9777
|
|
| BLAKE2b-256 |
38b3d532136e2177e315a5fea12ed894dd19ac00041b9f543951e5b280ec4aed
|