Schema-based environment variable management for Python projects
Project description
epicenv
Schema-based environment variable management for Python projects.
Define your environment variables in pyproject.toml with types, defaults, and help text. Use epicenv to create, validate, and manage .env files. Works with any Python project - Django, FastAPI, Flask, or plain Python.
Features
- 📋 Schema-based: Define variables in
pyproject.tomlwith types, defaults, and documentation - ✅ Validation: Catch undefined variables in debug mode before they cause runtime errors
- 🛠️ CLI Tools: Create and compare
.envfiles withuvx epicenv createanduvx epicenv diff - 🎯 Framework Agnostic: Works with Django, FastAPI, Flask, or any Python project
- 🔧 Flexible: Use schema-only, code-only, or hybrid approaches
- 🐍 Type Safe: Full type support (str, bool, int, list, url, json, etc.)
Quick Start
1. Define your schema in pyproject.toml
[tool.epicenv.variables]
SECRET_KEY = {
type = "str",
required = true,
help_text = "Secret key for cryptographic signing",
initial_func = "secrets.token_urlsafe"
}
DEBUG = {
type = "bool",
default = false,
initial = "on",
help_text = "Enable debug mode (never enable in production)"
}
DATABASE_URL = {
type = "dj_db_url",
default = "sqlite:///db.sqlite3",
help_text = "Database connection URL"
}
API_KEY = {
type = "str",
required = true,
help_text = "API key for external service"
}
2. Create a .env file
Use uvx to run without installing:
uvx epicenv create
Or install and use directly:
uv add epicenv
epicenv create
This generates a .env file with help text, types, and initial values:
# This is an initial .env file generated on 2026-01-24...
# Any environment variable with a default can be safely removed or commented out.
# Secret key for cryptographic signing
# type: str
SECRET_KEY=hkXzH0ZFG1YOtKhDsXAqS7rkt50_fZu_bV8YzPXr7VA
# Enable debug mode (never enable in production)
# type: bool
# default: False
DEBUG=on
# Database connection URL
# type: dj_db_url
# default: sqlite:///db.sqlite3
# DATABASE_URL=
# API key for external service
# type: str
API_KEY=
3. Use in your code
from epicenv import Env
env = Env()
env.read_env() # Load from .env file
SECRET_KEY = env.str("SECRET_KEY")
DEBUG = env.bool("DEBUG", default=False)
DATABASE_URL = env.str("DATABASE_URL", default="sqlite:///db.sqlite3")
API_KEY = env.str("API_KEY")
4. Validate your environment
Check for missing required variables:
epicenv validate
Compare your .env file with the schema:
epicenv diff
Installation
For any Python project
uv add epicenv
For Django projects (with dj-database-url, dj-email-url support)
uv add epicenv[django]
Without installation (using uvx)
uvx epicenv create
uvx epicenv diff
uvx epicenv validate
CLI Commands
epicenv create
Create a .env file from your pyproject.toml schema.
epicenv create # Create .env in current directory
epicenv create --path config/.env # Create at specific path
epicenv create --no-backup # Don't backup existing file
epicenv diff
Compare your .env file with the schema and show differences.
epicenv diff # Compare .env with schema
epicenv diff --path config/.env # Compare specific file
Output:
✓ All variables are in sync!
or
Missing required variables:
• API_KEY - API key for external service
Missing optional variables (have defaults):
• DATABASE_URL (default: sqlite:///db.sqlite3) - Database connection URL
Variables in .env file not defined in schema:
• OLD_VARIABLE
epicenv validate
Validate current environment against schema.
epicenv validate # Validate environment
epicenv validate --strict # Exit with error code if validation fails
Validation Mode
Validation is automatically enabled when DEBUG=true. Control validation behavior with the EPICENV_VALIDATE environment variable:
auto(default): Validate whenDEBUG=true, otherwise no validationstrict: Always validate and raise errors for undefined variableswarn: Always validate but only warn (don't raise errors)off: Disable validation completely
# Auto mode (default) - validates only when DEBUG=true
DEBUG=true python manage.py runserver # Validates!
DEBUG=false python manage.py runserver # No validation
# Force strict validation regardless of DEBUG
EPICENV_VALIDATE=strict python manage.py runserver
# Warn about undefined variables but don't fail
EPICENV_VALIDATE=warn python manage.py runserver
# Disable validation completely
EPICENV_VALIDATE=off python manage.py runserver
In your code:
from epicenv import Env
env = Env() # Validation mode controlled by EPICENV_VALIDATE env var
env.read_env()
# If MY_VAR is not in pyproject.toml schema and validation is enabled:
value = env.str("MY_VAR") # UndefinedVariableError!
Error message:
UndefinedVariableError: Environment variable 'MY_VAR' is not defined in pyproject.toml schema.
Add it to [tool.epicenv.variables] in your pyproject.toml:
[tool.epicenv.variables]
MY_VAR = { type = "str", help_text = "Description here" }
Or disable validation by setting EPICENV_VALIDATE=off
Schema Reference
Field Types
All environs types are supported:
- Basic:
str,bool,int,float,decimal - Collections:
list,dict,json - Date/Time:
date,datetime,time,timedelta - Specialized:
url,uuid,path,enum,log_level - Django (with
[django]extra):dj_db_url,dj_email_url,dj_cache_url
Schema Fields
[tool.epicenv.variables]
MY_VARIABLE = {
type = "str", # Variable type (required)
required = true, # Is this variable required? (default: true if no default)
default = "value", # Default value for .env generation and diff
help_text = "Description", # Documentation for this variable
initial = "initial_value", # Static initial value for .env generation
initial_func = "module.function" # Callable for dynamic initial values
}
Understanding default vs Runtime Defaults
The default field in pyproject.toml is only used for .env file generation and diff commands. It is not used at runtime.
Runtime defaults must always be specified in your Python code:
# Runtime default is specified here, not in pyproject.toml
DEBUG = env.bool("DEBUG", default=False)
# This allows for dynamic defaults based on other values
SECURE_COOKIES = env.bool("SECURE_COOKIES", default=not DEBUG)
This design prevents confusion about which default takes precedence and allows for dynamic defaults that depend on other values.
Initial Value Functions
Use initial_func to generate dynamic values:
[tool.epicenv.variables]
SECRET_KEY = {
type = "str",
initial_func = "secrets.token_urlsafe" # Python stdlib
}
DJANGO_SECRET = {
type = "str",
initial_func = "django.core.management.utils.get_random_secret_key"
}
CUSTOM_VALUE = {
type = "str",
initial_func = "myapp.utils.generate_api_key" # Your own function
}
Built-in Initializers
epicenv includes built-in initializer functions in the epicenv.initializers module:
epicenv.initializers.url_safe_password - Generate a URL-safe random password (letters, digits, -, _):
[tool.epicenv.variables]
SECRET_KEY = {
type = "str",
required = true,
help_text = "Secret key for cryptographic signing",
initial_func = "epicenv.initializers.url_safe_password"
}
# With custom length (default is 50)
API_TOKEN = {
type = "str",
required = true,
help_text = "API authentication token",
initial_func = "epicenv.initializers.url_safe_password",
kwargs = { length = 32 }
}
epicenv.initializers.onepassword - Fetch secrets from 1Password CLI:
Automatically retrieves secrets from 1Password using the op CLI tool during .env file generation. If 1Password is unavailable, it uses a descriptive fallback value.
[tool.epicenv.variables]
# Basic usage - fetches from 1Password, uses smart fallback if unavailable
STRIPE_API_KEY = {
type = "str",
required = true,
help_text = "Stripe API key for payments",
initial_func = "epicenv.initializers.onepassword",
args = ["op://Production/Stripe/api_key"]
}
# With custom fallback for local development
DATABASE_PASSWORD = {
type = "str",
required = true,
help_text = "Database password",
initial_func = "epicenv.initializers.onepassword",
args = ["op://Production/Database/password"],
kwargs = { fallback = "local_dev_password" }
}
# Silent mode (no warnings)
OPTIONAL_SECRET = {
type = "str",
default = "",
initial_func = "epicenv.initializers.onepassword",
args = ["op://Development/Optional/secret"],
kwargs = { silent = true, fallback = "" }
}
Requirements:
- 1Password CLI (
op) installed and in PATH - Signed in to 1Password (
op signin)
Behavior:
- When 1Password is available: Fetches the secret from your vault
- When 1Password is unavailable: Uses fallback value (custom or auto-generated
[Enter VARIABLE_NAME]) - Displays helpful warnings with setup instructions when 1Password is not available
1Password Reference Format: The reference string follows 1Password's secret reference format:
op://vault/item/field- Basic formatop://vault/item/section/field- With section
Examples:
op://Production/AWS/access_key_idop://Development/API Keys/stripe/production_key
Setup Instructions:
- Install 1Password CLI: https://developer.1password.com/docs/cli/get-started/
- Sign in:
op signin - Generate your
.envfile:epicenv create
Troubleshooting:
"1Password CLI not installed"
- Install the CLI from https://developer.1password.com/docs/cli/get-started/
- Verify with:
op --version
"Not signed in to 1Password CLI"
- Run:
op signin - Follow the prompts to authenticate
"Failed to read secret: vault not found"
- Check that the vault name is correct
- Verify you have access to the vault in your 1Password account
"Timeout reading from 1Password"
- Check your internet connection
- Try signing in again:
op signin
Custom Initializers
You can create your own initializer functions anywhere in your codebase:
# myapp/env_utils.py
import secrets
def generate_api_key() -> str:
"""Generate an API key with a custom prefix."""
return f"sk_{secrets.token_hex(16)}"
def generate_uuid() -> str:
"""Generate a UUID string."""
import uuid
return str(uuid.uuid4())
Then reference them in your schema:
[tool.epicenv.variables]
API_KEY = {
type = "str",
required = true,
initial_func = "myapp.env_utils.generate_api_key"
}
INSTANCE_ID = {
type = "str",
required = true,
initial_func = "myapp.env_utils.generate_uuid"
}
Django Integration
Option 1: Schema-based (Recommended)
Define variables in pyproject.toml and use the Env class:
# settings.py
from pathlib import Path
from epicenv import Env
BASE_DIR = Path(__file__).resolve().parent.parent
env = Env() # Validation automatically enabled when DEBUG=true
env.read_env(BASE_DIR / ".env")
SECRET_KEY = env.str("SECRET_KEY")
DEBUG = env.bool("DEBUG", default=False)
DATABASES = {"default": env.dj_db_url("DATABASE_URL", default="sqlite:///db.sqlite3")}
Then use CLI commands:
epicenv create # Instead of ./manage.py create_env_file
epicenv diff # Instead of ./manage.py diff_env_file
Option 2: Django Management Commands (Legacy)
For backward compatibility, Django management commands still work:
# settings.py
INSTALLED_APPS = [
...
"epicenv", # Add to INSTALLED_APPS
]
from epicenv import Env
env = Env()
env.read_env(BASE_DIR / ".env")
# Use help_text, initial, initial_func as before
SECRET_KEY = env.str(
"SECRET_KEY",
initial_func="django.core.management.utils.get_random_secret_key",
help_text="Django's secret key",
)
Then use Django commands:
./manage.py create_env_file
./manage.py diff_env_file
Examples
FastAPI Project
# pyproject.toml
[tool.epicenv.variables]
APP_NAME = { type = "str", default = "My API", help_text = "Application name" }
API_HOST = { type = "str", default = "0.0.0.0", help_text = "API host" }
API_PORT = { type = "int", default = 8000, help_text = "API port" }
DATABASE_URL = { type = "url", required = true, help_text = "Database URL" }
REDIS_URL = { type = "url", default = "redis://localhost:6379", help_text = "Redis URL" }
LOG_LEVEL = { type = "log_level", default = "INFO", help_text = "Logging level" }
# config.py
from epicenv import Env
env = Env()
env.read_env()
APP_NAME = env.str("APP_NAME", default="My API")
API_HOST = env.str("API_HOST", default="0.0.0.0")
API_PORT = env.int("API_PORT", default=8000)
DATABASE_URL = env.url("DATABASE_URL")
REDIS_URL = env.url("REDIS_URL", default="redis://localhost:6379")
LOG_LEVEL = env.log_level("LOG_LEVEL", default="INFO")
Flask Project
# pyproject.toml
[tool.epicenv.variables]
FLASK_APP = { type = "str", default = "app.py", help_text = "Flask app entry point" }
FLASK_ENV = { type = "str", default = "production", help_text = "Flask environment" }
SECRET_KEY = { type = "str", required = true, help_text = "Flask secret key", initial_func = "secrets.token_hex" }
DATABASE_URL = { type = "url", required = true, help_text = "Database URL" }
# config.py
from epicenv import Env
env = Env()
env.read_env()
class Config:
FLASK_APP = env.str("FLASK_APP", default="app.py")
FLASK_ENV = env.str("FLASK_ENV", default="production")
SECRET_KEY = env.str("SECRET_KEY")
SQLALCHEMY_DATABASE_URI = env.url("DATABASE_URL")
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
Acknowledgments
epicenv is built on top of environs, which provides the core environment variable parsing functionality. We're grateful to Steven Loria and the environs contributors for their excellent work.
License
This project is licensed under the MIT License.
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 epicenv-1.2.0.tar.gz.
File metadata
- Download URL: epicenv-1.2.0.tar.gz
- Upload date:
- Size: 48.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5491174f31cfdfeea7cd2e345354c998871e67dc24150f8659166cfce0cb5669
|
|
| MD5 |
b8c03a5b55d45023469c6971b2eb7a40
|
|
| BLAKE2b-256 |
fa6e39b47d0117abbab00a1fbe6a5bdb4fb7dd2fb77c9d5565e50e6214730190
|
File details
Details for the file epicenv-1.2.0-py3-none-any.whl.
File metadata
- Download URL: epicenv-1.2.0-py3-none-any.whl
- Upload date:
- Size: 21.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa20d9d6829b8bf3df7b59a1b58489ff53e46973766e3b5985378a0a1e63b5ba
|
|
| MD5 |
1bb7bcc2942bcbdad2239e86f6918484
|
|
| BLAKE2b-256 |
4d3eb5d74bdcbc9695f969c1b0f7c48c5eb4476185946acbbd86d9962f876633
|