Type-safe environment configuration with automatic .env file loading
Project description
dotenvmodel
Type-safe environment configuration with automatic .env file loading
dotenvmodel is a Python library that provides type-safe environment configuration with automatic .env file loading. It combines the familiar developer experience of Pydantic-style field definitions with intelligent .env file cascading inspired by Node.js dotenv patterns.
Features
- Minimal Dependencies: Only requires
python-dotenv - Type Safety: Full type hint support with automatic type coercion
- Rich Type Support: UUID, Decimal, datetime, timedelta, SecretStr, HttpUrl, PostgresDsn, RedisDsn, Json[T], and more
- Developer Experience: Intuitive Pydantic-style API
- Smart .env Loading: Automatic cascading of
.env,.env.{env},.env.{env}.localfiles - Configuration Reload: Reload configuration at runtime without creating new instances
- Configuration Documentation: Generate docs in multiple formats (table, markdown, JSON, HTML, dotenv) with
describe() - .env.example Generation: Automatically generate
.env.examplefiles with type hints, constraints, and examples - File Export: Save documentation directly to files for integration with build tools and wikis
- Environment Prefixes: Class-level
env_prefixto namespace environment variables - Validation: Numeric constraints (ge, le, gt, lt), string constraints (min_length, max_length, regex), choice validation, and collection size constraints (min_items, max_items)
- Clear Error Messages: Helpful validation errors that guide you to fixes
- Optional Logging: Built-in logging support to debug configuration loading
- Zero Runtime Overhead: All validation happens at startup/load time
Installation
pip install dotenvmodel
Or with uv:
uv add dotenvmodel
Quick Start
from dotenvmodel import DotEnvConfig, Field
class AppConfig(DotEnvConfig):
# Required fields (Pydantic-style)
database_url: str = Field(...)
api_key: str = Field(...)
# Optional with defaults
debug: bool = Field(default=False)
port: int = Field(default=8000, ge=1, le=65535)
workers: int = Field(default=4, ge=1, le=16)
# Collection types
allowed_hosts: list[str] = Field(default_factory=list)
# Load configuration from .env files
config = AppConfig.load(env="dev")
# Access configuration with full type safety and IntelliSense
print(f"Connecting to {config.database_url}") # config.database_url: str
print(f"Running on port {config.port}") # config.port: int
print(f"Debug mode: {config.debug}") # config.debug: bool
# Generate documentation for your configuration
print(AppConfig.describe())
Type Safety and IntelliSense
dotenvmodel provides full type safety - your IDE and type checkers (mypy, pyright) understand the types of your configuration fields:
class AppConfig(DotEnvConfig):
database_url: str = Field()
port: int = Field(default=8000)
debug: bool = Field(default=False)
config = AppConfig.load()
# ✅ Type checkers know these types:
db_url: str = config.database_url # ✅ Correct: str = str
port_num: int = config.port # ✅ Correct: int = int
is_debug: bool = config.debug # ✅ Correct: bool = bool
# ❌ Type checker errors:
wrong: int = config.database_url # ❌ Error: str is not compatible with int
wrong: str = config.debug # ❌ Error: bool is not compatible with str
Your IDE will provide:
- Autocomplete for all config fields
- Type hints showing field types
- Error detection for type mismatches
- Go to definition support
Supported Types
Basic Types
String
class Config(DotEnvConfig):
name: str = Field()
# Environment: NAME=myapp
# Result: config.name == "myapp"
Integer
class Config(DotEnvConfig):
port: int = Field(default=8000)
# Environment: PORT=3000
# Result: config.port == 3000 (int)
Float
class Config(DotEnvConfig):
timeout: float = Field(default=30.0)
# Environment: TIMEOUT=60.5
# Result: config.timeout == 60.5 (float)
Boolean
Supports multiple formats for true/false values:
class Config(DotEnvConfig):
debug: bool = Field(default=False)
# True values: "true", "1", "yes", "on", "t", "y" (case-insensitive)
# False values: "false", "0", "no", "off", "f", "n", "" (case-insensitive)
Path
from pathlib import Path
class Config(DotEnvConfig):
config_path: Path = Field(default=Path("/etc/app"))
# Environment: CONFIG_PATH=/opt/myapp/config
# Result: config.config_path == Path("/opt/myapp/config")
Collection Types
List
class Config(DotEnvConfig):
# List of strings
hosts: list[str] = Field(default_factory=list)
# Environment: HOSTS=localhost,example.com,*.example.com
# Result: config.hosts == ["localhost", "example.com", "*.example.com"]
# List of integers
ports: list[int] = Field(default_factory=list)
# Environment: PORTS=8000,8001,8002
# Result: config.ports == [8000, 8001, 8002]
# Custom separator
tags: list[str] = Field(default_factory=list, separator=";")
# Environment: TAGS=web;api;backend
# Result: config.tags == ["web", "api", "backend"]
Set
class Config(DotEnvConfig):
roles: set[str] = Field(default_factory=set)
# Environment: ROLES=admin,user,admin
# Result: config.roles == {"admin", "user"}
Tuple
class Config(DotEnvConfig):
coordinates: tuple[str, ...] = Field()
# Environment: COORDINATES=x,y,z
# Result: config.coordinates == ("x", "y", "z")
Dictionary
class Config(DotEnvConfig):
headers: dict[str, str] = Field(default_factory=dict)
# Environment: HEADERS=Content-Type=application/json,Accept=*/*
# Result: config.headers == {"Content-Type": "application/json", "Accept": "*/*"}
Advanced Types
UUID
from uuid import UUID
class Config(DotEnvConfig):
tenant_id: UUID = Field()
# Environment: TENANT_ID=550e8400-e29b-41d4-a716-446655440000
# Result: config.tenant_id == UUID('550e8400-e29b-41d4-a716-446655440000')
Decimal (for precise arithmetic)
from decimal import Decimal
class Config(DotEnvConfig):
price: Decimal = Field()
tax_rate: Decimal = Field(ge=Decimal('0'), le=Decimal('1'))
# Environment: PRICE=19.99, TAX_RATE=0.0825
# Result: config.price == Decimal('19.99')
Datetime and Timedelta
from datetime import datetime, timedelta
class Config(DotEnvConfig):
created_at: datetime = Field()
# Environment: CREATED_AT=2025-01-15T10:30:00
# Result: config.created_at == datetime(2025, 1, 15, 10, 30, 0)
cache_ttl: timedelta = Field()
# Environment: CACHE_TTL=1h30m (or: 5400 for seconds)
# Result: config.cache_ttl == timedelta(hours=1, minutes=30)
# Supports: ms, s, m, h, d, w
SecretStr (hide sensitive data)
from dotenvmodel.types import SecretStr
class Config(DotEnvConfig):
api_key: SecretStr = Field(min_length=32)
password: SecretStr = Field()
# Hides value in logs and repr
config = Config.load()
print(config.api_key) # SecretStr('**********')
print(config.api_key.get_secret_value()) # 'actual-secret-key'
URL and DSN Types
from dotenvmodel.types import HttpUrl, PostgresDsn, RedisDsn
class Config(DotEnvConfig):
api_url: HttpUrl = Field()
# Environment: API_URL=https://api.example.com/v1
# Validates scheme, provides parsed components
database_url: PostgresDsn = Field()
# Environment: DATABASE_URL=postgresql://user:pass@localhost:5432/db
redis_url: RedisDsn = Field()
# Environment: REDIS_URL=redis://localhost:6379/0
# URL types work like strings but provide properties:
config = Config.load()
print(config.api_url.host) # 'api.example.com'
print(config.api_url.port) # 443
print(config.database_url.database) # 'db'
print(config.redis_url.database) # 0
JSON Parsing
from dotenvmodel.types import Json
class Config(DotEnvConfig):
feature_flags: Json[dict[str, bool]] = Field()
# Environment: FEATURE_FLAGS={"new_ui": true, "beta_api": false}
allowed_roles: Json[list[str]] = Field()
# Environment: ALLOWED_ROLES=["admin", "user", "guest"]
config = Config.load()
assert config.feature_flags == {"new_ui": True, "beta_api": False}
Optional Types
Optional types automatically default to None if no explicit default is provided:
from typing import Optional
class Config(DotEnvConfig):
# These automatically default to None (no need for explicit default=None)
optional_value: str | None = Field()
optional_port: int | None = Field()
# Using Optional from typing also works
optional_name: Optional[str] = Field()
# You can still provide explicit defaults if needed
optional_with_default: str | None = Field(default="custom")
Field Definitions
Defining Required Fields
from dotenvmodel import DotEnvConfig, Field, Required
class Config(DotEnvConfig):
# Method 1: Pydantic-style Field(...) - Recommended
api_key: str = Field(...)
# Method 2: Field() with no default - Also works
database_url: str = Field()
# Method 3: Required sentinel - Alternative
secret: str = Required
All three methods work identically at runtime and have no type checker issues. We recommend Field(...) as it's consistent with Pydantic's API and makes it explicit that you're defining a field.
Optional Fields with Defaults
class Config(DotEnvConfig):
# Simple default
port: int = Field(default=8000)
# Default factory for mutable defaults
hosts: list[str] = Field(default_factory=list)
tags: dict[str, str] = Field(default_factory=dict)
Field Aliases
Use a different environment variable name than the field name:
class Config(DotEnvConfig):
# Field name: postgres_dsn
# Environment variable: DATABASE_URL
postgres_dsn: str = Field(alias="DATABASE_URL")
# Field name: api_token
# Environment variable: SECRET_TOKEN
api_token: str = Field(alias="SECRET_TOKEN")
Field Descriptions
Document your fields for better maintainability:
class Config(DotEnvConfig):
timeout: float = Field(
default=30.0,
description="Request timeout in seconds"
)
Validation
Numeric Validation
class Config(DotEnvConfig):
# Greater than or equal (>=)
min_connections: int = Field(ge=1)
# Less than or equal (<=)
max_connections: int = Field(le=100)
# Greater than (>)
timeout: float = Field(gt=0)
# Less than (<)
percentage: float = Field(lt=100.0)
# Combined constraints
port: int = Field(default=8000, ge=1, le=65535)
pool_size: int = Field(default=10, ge=1, le=100)
String Validation
class Config(DotEnvConfig):
# Minimum length
api_key: str = Field(min_length=32)
# Maximum length
username: str = Field(max_length=20)
# Regex pattern
email: str = Field(regex=r'^[\w\.-]+@[\w\.-]+\.\w+$')
# Combined constraints
password: str = Field(
min_length=8,
max_length=128,
regex=r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).+$'
)
Choice Validation
class Config(DotEnvConfig):
# Must be one of the specified values
environment: str = Field(
default="dev",
choices=["dev", "test", "staging", "prod"]
)
log_level: str = Field(
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
)
Loading Configuration
From Environment Variables
# Load from environment and .env files
config = AppConfig.load()
# Specify environment explicitly
config = AppConfig.load(env="prod")
# Override behavior
config = AppConfig.load(override=True) # .env files override env vars (default)
config = AppConfig.load(override=False) # Env vars take precedence
# Custom .env file directory
from pathlib import Path
config = AppConfig.load(env_dir=Path("/app/config"))
.env File Cascading
Files are loaded in order (later files override earlier ones):
.env- Base configuration (usually gitignored).env.local- Local base overrides (gitignored, never committed).env.{env}- Environment-specific (committed to repo).env.{env}.local- Local environment overrides (gitignored, never committed)
Example:
# .env (base - usually gitignored)
DATABASE_URL=postgresql://localhost/myapp
REDIS_URL=redis://localhost:6379
DEBUG=false
# .env.local (local base overrides - gitignored)
DATABASE_URL=postgresql://localhost/myapp_local
# .env.dev (development - committed to repo)
DEBUG=true
LOG_LEVEL=DEBUG
# .env.dev.local (local dev overrides - gitignored)
ENABLE_PROFILING=true
API_KEY=dev-key-local-override
When you load with env="dev":
config = AppConfig.load(env="dev")
# Loads in order: .env → .env.local → .env.dev → .env.dev.local
# Final DATABASE_URL: postgresql://localhost/myapp_local (from .env.local)
# Final DEBUG: true (from .env.dev)
# Final ENABLE_PROFILING: true (from .env.dev.local)
From Dictionary (Testing)
# Load from dictionary for testing
config = AppConfig.load_from_dict({
"DATABASE_URL": "postgresql://localhost/test",
"API_KEY": "test-key",
"DEBUG": "true",
"PORT": "8000",
})
# Skip validation if needed
config = AppConfig.load_from_dict(data, validate=False)
Logging
dotenvmodel includes optional logging to help debug configuration issues. Logging is disabled by default but can be easily enabled.
Enable Logging
from dotenvmodel import configure_logging, DotEnvConfig, Field
# Enable INFO level logging
configure_logging("INFO")
class Config(DotEnvConfig):
database_url: str = Field()
config = Config.load()
Logging Output Example
2025-12-05 00:33:40 - dotenvmodel - INFO - Loading Config configuration
2025-12-05 00:33:40 - dotenvmodel - INFO - Loading configuration for environment: dev
2025-12-05 00:33:40 - dotenvmodel - INFO - Loading environment variables from .env
2025-12-05 00:33:40 - dotenvmodel - INFO - Loading environment variables from .env.dev
2025-12-05 00:33:40 - dotenvmodel - INFO - Successfully loaded 2 file(s): .env, .env.dev
2025-12-05 00:33:40 - dotenvmodel - INFO - Config configuration loaded successfully
Log Levels
# DEBUG - Most verbose, shows all operations
configure_logging("DEBUG")
# INFO - Shows file loading and configuration status
configure_logging("INFO")
# WARNING - Only shows warnings (e.g., missing files)
configure_logging("WARNING")
# ERROR - Only shows errors
configure_logging("ERROR")
Using Environment Variable
# Set via environment variable
export DOTENVMODEL_LOG_LEVEL=DEBUG
python your_app.py
Disable Logging
from dotenvmodel import disable_logging
disable_logging()
Custom Logging Configuration
import logging
from dotenvmodel import configure_logging
# Use custom format
configure_logging(
"INFO",
format_string="[%(levelname)s] %(message)s"
)
# Or configure directly with standard logging
logger = logging.getLogger("dotenvmodel")
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(message)s'))
logger.addHandler(handler)
Configuration Methods
Access as Dictionary
config = AppConfig.load()
config_dict = config.dict()
# {'database_url': 'postgresql://...', 'debug': True, 'port': 8000}
Get Method with Default
config = AppConfig.load()
timeout = config.get('timeout', 30) # Returns 30 if timeout not set
String Representation
config = AppConfig.load()
print(repr(config))
# AppConfig(database_url='postgresql://...', debug=True, port=8000)
Reload Configuration
Reload configuration from environment variables without creating a new instance:
# Load initial configuration
config = AppConfig.load(env="dev")
print(config.port) # 8000
# Later, when environment variables change...
os.environ["PORT"] = "9000"
# Reload the configuration
config.reload()
print(config.port) # 9000
# Reload reuses the original parameters by default
config = AppConfig.load(env="dev", override=True)
config.reload() # Uses env="dev", override=True
# Override any parameter during reload
config.reload(env="prod") # Switch to production environment
The reload() method:
- Reloads all fields from environment variables and .env files
- By default, reuses the same
env,override, andenv_dirparameters from the originalload()call - Allows overriding any parameter by passing new values
- Validates all fields and raises errors if validation fails
- Returns the same instance (useful for method chaining)
Configuration Documentation
Generate human-readable documentation for your configuration classes using the describe() method. This is useful for:
- Creating documentation for your team
- Generating
.env.examplefiles automatically - Validating configuration in CI pipelines
- Onboarding new developers quickly
Generate Documentation for a Single Config
from dotenvmodel import DotEnvConfig, Field
class AppConfig(DotEnvConfig):
database_url: str = Field(description="PostgreSQL connection string")
port: int = Field(default=8000, ge=1, le=65535, description="Server port")
debug: bool = Field(default=False, description="Enable debug mode")
workers: int = Field(default=4, ge=1, le=16, description="Number of worker processes")
# Generate ASCII table (default)
print(AppConfig.describe())
Output:
AppConfig
=========
+--------------+------+----------+---------+---------------------------+----------------+
| ENV Variable | Type | Required | Default | Description | Constraints |
+--------------+------+----------+---------+---------------------------+----------------+
| DATABASE_URL | str | Yes | - | PostgreSQL connection ... | - |
| PORT | int | No | 8000 | Server port | ge=1, le=65535 |
| DEBUG | bool | No | False | Enable debug mode | - |
| WORKERS | int | No | 4 | Number of worker proces...| ge=1, le=16 |
+--------------+------+----------+---------+---------------------------+----------------+
Output Formats
ASCII Table (default) - Best for terminal output and logging:
print(AppConfig.describe(output_format="table"))
Markdown - Perfect for README files and documentation:
# Generate markdown documentation
docs = AppConfig.describe(output_format="markdown")
# Save to file
with open("CONFIG.md", "w") as f:
f.write(docs)
JSON - Ideal for CI validation and programmatic processing:
import json
# Get configuration schema as JSON
config_spec = AppConfig.describe(output_format="json")
data = json.loads(config_spec)
# Use for validation, code generation, etc.
print(data["class_name"]) # "AppConfig"
print(data["fields"][0]["env_var"]) # "DATABASE_URL"
HTML - Styled output for web documentation:
# Generate HTML with styled tables
html_docs = AppConfig.describe(output_format="html")
# Save to file
with open("config.html", "w") as f:
f.write(html_docs)
Dotenv Format - For generating .env.example files:
# Generate .env.example format
dotenv_docs = AppConfig.describe(output_format="dotenv")
print(dotenv_docs)
File Export
Save documentation directly to files using the output parameter:
# Save as markdown
AppConfig.describe(output_format="markdown", output="docs/config.md")
# Save as HTML
AppConfig.describe(output_format="html", output="docs/config.html")
# Save as JSON
AppConfig.describe(output_format="json", output="config-schema.json")
Generate .env.example Files
Automatically generate .env.example files for onboarding new developers:
from dotenvmodel import DotEnvConfig, Field, SecretStr
class AppConfig(DotEnvConfig):
env_prefix = "APP_"
api_key: str = Field(
min_length=32,
max_length=64,
description="API key for external service"
)
port: int = Field(
default=8000,
ge=1,
le=65535,
description="Server port number"
)
database_password: SecretStr = Field(
default=SecretStr("change_me_in_production"),
min_length=8,
description="Database connection password"
)
allowed_hosts: list[str] = Field(
default_factory=list,
separator=";",
min_items=1,
max_items=10,
description="Allowed hostnames for CORS"
)
# Generate and print .env.example
print(AppConfig.generate_env_example())
# Or save directly to file
AppConfig.generate_env_example(output=".env.example")
Output in .env.example:
# Configuration for AppConfig
# All variables prefixed with: APP_
# API key for external service
# Type: str | Constraints: min_length=32, max_length=64
# Example: APP_API_KEY=your_value_here
APP_API_KEY=
# Server port number
# Type: int | Constraints: ge=1, le=65535
# Example: APP_PORT=8000
# APP_PORT=8000
# Database connection password
# Type: SecretStr | Constraints: min_length=8
# APP_DATABASE_PASSWORD=your_secret_here
# Allowed hostnames for CORS
# Type: list[str] | Constraints: min_items=1, max_items=10, separator=';'
# Example: APP_ALLOWED_HOSTS=[]
# APP_ALLOWED_HOSTS=[]
The .env.example file includes:
- Type information - Shows the expected Python type
- Parsing hints - Explains how to format complex types (e.g., "comma-separated values" for lists)
- Constraints - Documents validation rules (min/max length, numeric ranges, etc.)
- Examples - Shows example values for required fields
- Commented defaults - Optional fields are commented out with their default values
- Secret handling - SecretStr fields are masked appropriately
Document Multiple Configurations
Use describe_configs() to document multiple related configuration classes:
from dotenvmodel import DotEnvConfig, Field, describe_configs
class DatabaseConfig(DotEnvConfig):
env_prefix = "DB_"
host: str = Field(description="Database host")
port: int = Field(default=5432, description="Database port")
class RedisConfig(DotEnvConfig):
env_prefix = "REDIS_"
host: str = Field(description="Redis host")
port: int = Field(default=6379, description="Redis port")
# Generate documentation for all configs
all_docs = describe_configs([DatabaseConfig, RedisConfig], output_format="markdown")
print(all_docs)
Practical Use Cases
1. Generate .env.example files for onboarding:
# Generate .env.example with helpful comments and type information
AppConfig.generate_env_example(output=".env.example")
# Or combine multiple configs
from dotenvmodel import describe_configs
with open(".env.example", "w") as f:
f.write("# Application Configuration\n\n")
f.write("# Copy this file to .env and fill in the values\n\n")
for config_cls in [AppConfig, DatabaseConfig, RedisConfig]:
f.write(config_cls.generate_env_example())
f.write("\n\n")
2. CI Configuration Validation:
import json
import os
# Get required environment variables from config schema
spec = json.loads(AppConfig.describe(output_format="json"))
required_vars = [f["env_var"] for f in spec["fields"] if f["required"]]
# Validate all required vars are set
missing = [var for var in required_vars if var not in os.environ]
if missing:
print(f"ERROR: Missing required environment variables: {', '.join(missing)}")
exit(1)
3. Developer Onboarding:
import os
# Display configuration reference in development mode
if os.getenv("ENV") == "dev":
print("\n" + "=" * 80)
print("CONFIGURATION REFERENCE")
print("=" * 80)
print(AppConfig.describe())
print("=" * 80 + "\n")
4. Generate Documentation Website:
from dotenvmodel import describe_configs
# Generate markdown docs for all config classes
configs = [AppConfig, DatabaseConfig, RedisConfig, CacheConfig]
# Save as markdown
describe_configs(configs, output_format="markdown", output="docs/configuration.md")
# Or generate HTML version with styling
describe_configs(configs, output_format="html", output="docs/configuration.html")
5. Build Tool Integration:
# build_docs.py - Run during build process
from your_app.config import AppConfig, DatabaseConfig
# Generate .env.example for repository
AppConfig.generate_env_example(output=".env.example")
# Generate markdown docs
AppConfig.describe(output_format="markdown", output="docs/CONFIG.md")
# Generate HTML for internal wiki
AppConfig.describe(output_format="html", output="docs/config.html")
print("✓ Configuration documentation generated")
Environment Variable Prefixes
Use class-level prefixes to namespace environment variables:
class DatabaseConfig(DotEnvConfig):
env_prefix = "DB_" # All fields will be prefixed with DB_
host: str = Field()
port: int = Field(default=5432)
name: str = Field()
# Reads DB_HOST, DB_PORT, DB_NAME from environment
config = DatabaseConfig.load_from_dict({
"DB_HOST": "localhost",
"DB_PORT": "5433",
"DB_NAME": "myapp"
})
Prefix Behavior
-
Automatic Uppercasing: Field names are automatically uppercased and prefixed
host→DB_HOSTport→DB_PORT
-
Aliases Override Prefix: When using
alias, the prefix is NOT applied (aliases are absolute)class Config(DotEnvConfig): env_prefix = "APP_" db_url: str = Field(alias="DATABASE_URL") # Reads DATABASE_URL (no prefix) api_key: str = Field() # Reads APP_API_KEY (with prefix)
-
No Prefix by Default: If
env_prefixis not set, no prefix is appliedclass Config(DotEnvConfig): # No env_prefix defined host: str = Field() # Reads HOST
Multiple Config Classes with Different Prefixes
class DatabaseConfig(DotEnvConfig):
env_prefix = "DB_"
host: str = Field()
port: int = Field(default=5432)
class RedisConfig(DotEnvConfig):
env_prefix = "REDIS_"
host: str = Field()
port: int = Field(default=6379)
class AppConfig(DotEnvConfig):
env_prefix = "APP_"
name: str = Field()
version: str = Field()
# Each config reads its own prefixed variables
db = DatabaseConfig.load() # Reads DB_HOST, DB_PORT
redis = RedisConfig.load() # Reads REDIS_HOST, REDIS_PORT
app = AppConfig.load() # Reads APP_NAME, APP_VERSION
Error Handling
Missing Required Field
try:
config = AppConfig.load()
except MissingFieldError as e:
print(e)
# MissingFieldError: Required field 'api_key' is not set.
#
# Environment variable name: API_KEY
# Field type: str
# Hint: Set API_KEY in your environment or .env file
Type Coercion Error
try:
config = AppConfig.load_from_dict({"PORT": "abc"})
except TypeCoercionError as e:
print(e)
# TypeCoercionError: Failed to coerce field 'port' to type int.
#
# Value: "abc"
# Environment variable: PORT
# Error: invalid literal for int() with base 10: 'abc'
# Hint: Ensure PORT contains a valid int
Validation Constraint Error
try:
config = AppConfig.load_from_dict({"PORT": "99999"})
except ConstraintViolationError as e:
print(e)
# ConstraintViolationError: Field 'port' violates constraint.
#
# Value: 99999
# Constraint: le=65535
# Error: Value must be less than or equal to 65535
# Hint: Set PORT to a value that satisfies the constraint
Advanced Examples
Complete Application Configuration
from pathlib import Path
from dotenvmodel import DotEnvConfig, Field, Required
class DatabaseConfig(DotEnvConfig):
env_prefix = "DB_" # Namespace with DB_ prefix
host: str = Field()
port: int = Field(default=5432)
name: str = Field()
pool_size: int = Field(default=10, ge=1, le=100)
pool_timeout: float = Field(default=30.0, gt=0)
echo: bool = Field(default=False)
class RedisConfig(DotEnvConfig):
env_prefix = "REDIS_" # Namespace with REDIS_ prefix
host: str = Field()
port: int = Field(default=6379)
password: str | None = Field(default=None)
db: int = Field(default=0, ge=0, le=15)
socket_keepalive: bool = Field(default=True)
class AppConfig(DotEnvConfig):
env_prefix = "APP_" # Namespace with APP_ prefix
# App settings
environment: str = Field(
default="dev",
choices=["dev", "test", "staging", "prod"]
)
debug: bool = Field(default=False)
secret_key: str = Field(min_length=32)
# Server settings
host: str = Field(default="0.0.0.0")
port: int = Field(default=8000, ge=1, le=65535)
workers: int = Field(default=4, ge=1)
# External services (using alias to override prefix)
api_base_url: str = Field(alias="API_BASE_URL")
api_timeout: float = Field(default=30.0, ge=0.1, le=300.0)
# Feature flags
enable_caching: bool = Field(default=True)
enable_metrics: bool = Field(default=False)
# Lists and paths
allowed_origins: list[str] = Field(default_factory=list)
upload_dir: Path = Field(default=Path("/tmp/uploads"))
# Load all configs with prefixes
# DatabaseConfig reads: DB_HOST, DB_PORT, DB_NAME, etc.
# RedisConfig reads: REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, etc.
# AppConfig reads: APP_ENVIRONMENT, APP_DEBUG, APP_HOST, API_BASE_URL (alias), etc.
db_config = DatabaseConfig.load(env="prod")
redis_config = RedisConfig.load(env="prod")
app_config = AppConfig.load(env="prod")
# Reload configuration when environment changes
# (e.g., after receiving SIGHUP signal or config update)
db_config.reload() # Reloads with same env="prod"
redis_config.reload()
app_config.reload()
Testing Configuration
import pytest
from dotenvmodel import DotEnvConfig, Field, Required, MissingFieldError
class TestConfig(DotEnvConfig):
database_url: str = Required
api_key: str = Required
debug: bool = Field(default=False)
def test_load_from_dict():
config = TestConfig.load_from_dict({
"database_url": "sqlite:///:memory:",
"api_key": "test-key-123",
"debug": "true",
})
assert config.database_url == "sqlite:///:memory:"
assert config.api_key == "test-key-123"
assert config.debug is True
def test_missing_required_field():
with pytest.raises(MissingFieldError) as exc_info:
TestConfig.load_from_dict({"api_key": "test"})
assert "database_url" in str(exc_info.value)
@pytest.fixture
def test_config():
"""Fixture providing test configuration."""
return TestConfig.load_from_dict({
"database_url": "sqlite:///:memory:",
"api_key": "test-key",
})
def test_with_fixture(test_config):
assert test_config.database_url == "sqlite:///:memory:"
Best Practices
-
Use Type Hints: Always specify type hints for proper validation
port: int = Field(default=8000) # ✓ Good port = Field(default=8000) # ✗ Bad - no type hint
-
Use Validation: Add constraints to catch configuration errors early
port: int = Field(default=8000, ge=1, le=65535)
-
Use Aliases: Keep environment variable names consistent with conventions
postgres_dsn: str = Field(alias="DATABASE_URL")
-
Use Default Factories: For mutable defaults like lists and dicts
hosts: list[str] = Field(default_factory=list) # ✓ Good hosts: list[str] = Field(default=[]) # ✗ Bad - mutable default
-
Document Fields: Use descriptions for complex configurations
timeout: float = Field( default=30.0, ge=0.1, description="API request timeout in seconds" )
Requirements
- Python 3.12+
- python-dotenv
Known Limitations
Union Types (Non-Optional)
Non-optional Union types like str | int or Union[str, int] are not currently supported. Only Optional unions (types with None) work:
# ✅ Supported - Optional unions
class Config(DotEnvConfig):
value: str | None = Field() # Works
other: int | None = Field() # Works
# ❌ Not supported - Non-optional unions
class Config(DotEnvConfig):
value: str | int = Field() # Not supported
Workaround: Use a single type (typically str) and handle conversion in your application code:
class Config(DotEnvConfig):
value: str = Field()
config = Config.load()
# Convert to int if needed in your code
value_as_int = int(config.value) if config.value.isdigit() else config.value
License
MIT License - see LICENSE for details.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Links
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 dotenvmodel-0.2.0.tar.gz.
File metadata
- Download URL: dotenvmodel-0.2.0.tar.gz
- Upload date:
- Size: 112.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8971b7633016ccf3a5da2bc49f327da9d76a3d97903ce7265061d6ce79c0424
|
|
| MD5 |
bdb664c4b52d9bdf0712eb8032454ad3
|
|
| BLAKE2b-256 |
8e6aaf72bacaec688ccd736b497fed9869303c46bfe61cccfeef3245d7538d58
|
File details
Details for the file dotenvmodel-0.2.0-py3-none-any.whl.
File metadata
- Download URL: dotenvmodel-0.2.0-py3-none-any.whl
- Upload date:
- Size: 40.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88978fbb10552832d621e8768595b0b38b685f0ce630052eae2e139e585f91b5
|
|
| MD5 |
7306aa6fe9509b184695e9dc35206991
|
|
| BLAKE2b-256 |
44f4eb3b63a02968e91dc6a5ca1a06dcb95d2fd907ad1dc05bc58fc01b047da1
|