Configuration management for Python applications. Elegant, type-safe, and built for real-world needs.
PyConfigre bridges the gap between manual configuration management and heavyweight tools. It combines Pydantic's robust validation with support for multiple formats (YAML, JSON, TOML), environment variables, and a fluent API that reads like natural Python. Need the pipeline without a schema? PyConfigre has you covered with RawConfigBuilder. Whether you're building microservices, web applications, or CLI tools, PyConfigre handles configuration without getting in the way.
Configuration shouldn't be complicated. Most tools either do too much or too little. PyConfigre fits the middle ground:
- For teams who want type safety without sacrificing simplicity
- For projects that need to merge configurations from multiple sources
- For developers who prefer explicit, readable code over magic configuration files
- 📋 Multi-Format Loading: YAML, JSON, TOML, environment variables, or Python dictionaries
- ✓ Type-Safe Validation: Full Pydantic integration for bulletproof configuration
- 🔀 Smart Priority Handling: Effortlessly merge multiple sources (file → env → dict)
- ⛓️ Fluent API: Chainable methods that read naturally
- 🎯 Format Auto-Detection: Automatically identify and parse files
- 🐍 Modern Python: Full type hints and Python 3.10+ syntax
- ** Schema-Free Option**: Use
RawConfigBuilderwhen you just need a mergeddict— no Pydantic required - 🪶 Lightweight: Only
pydanticandpyyamlrequired for core functionality
# Basic installation (JSON & YAML support)
pip install pyconfigre
# Add TOML support
pip install pyconfigre[toml]
# With uv support
uv add pyconfigrefrom pydantic import BaseModel
from pyconfigre import ConfigBuilder
class AppConfig(BaseModel):
"""Your application configuration schema."""
debug: bool = False
port: int = 8000
host: str = "0.0.0.0"
database_url: str
# Load from file → environment → runtime overrides
config = (
ConfigBuilder(AppConfig)
.from_file("config.yaml")
.from_env("MYAPP_")
.from_dict({"debug": True})
.build()
)
# Type-safe access with IDE autocomplete
print(f"Server: {config.host}:{config.port}")That's it. Three lines of configuration loading. Three sources merged intelligently. Full type safety throughout.
Don't need typed validation? Use RawConfigBuilder to get a plain dictionary:
from pyconfigre import RawConfigBuilder
raw_config = (
RawConfigBuilder()
.from-file("config.yaml")
.from_env("MYAPP_")
.set("debug", True) # Explicit override
.build_dict()
)
# config is a plain dict — no schema needed
print(f"Port: {raw_config['port']}")Same pipeline, same priority system, same multi-format support — just without the Pydantic model.
The most common real-world pattern:
config = (
ConfigBuilder(AppConfig)
.from_file("config/default.yaml") # Base configuration
.from_env("MYAPP_") # Environment overrides
.from_dict(runtime_overrides) # Runtime values (highest priority)
.build()
)Priority order (lowest to highest): File → Environment → Dict → Explicit .set() method
Load different configurations per environment:
import os
environment = os.getenv("ENV", "development")
config = (
ConfigBuilder(AppConfig)
.from_file(f"config/{environment}.yaml") # Environment-specific base
.from_file("config/local.yaml", optional=True) # Local dev overrides
.from_env("MYAPP_")
.build()
)Structure complex configurations with nested Pydantic models:
from pydantic import BaseModel, Field
class DatabaseConfig(BaseModel):
host: str
port: int = 5432
username: str
password: str
database: str
class CacheConfig(BaseModel):
enabled: bool = True
ttl_seconds: int = 3600
backend: str = "redis"
class AppConfig(BaseModel):
app_name: str
debug: bool = False
database: DatabaseConfig
cache: CacheConfig = Field(default_factory=CacheConfig)
config = ConfigBuilder(AppConfig).from_file("config.yaml").build()
# Access nested values with full type safety
print(f"DB: {config.database.host}:{config.database.port}")
print(f"Cache TTL: {config.cache.ttl_seconds}s")use RawConfigBuilder when you need the pipeline but not the validation — scripts, CLI tools, migrations, or quick prototypes:
from pyconfigre import RawConfigBuilder
# Full pipeline, plain dict output
raw_config = (
RawConfigBuilder()
.from_file("config/default.yaml")
.from_env("MYAPP_")
.from_dict({"timeout": 30})
.build_dict()
)
# inspect mid-chain without finalising
raw_builder = RawConfigBuilder().from_file("config.yaml")
print(raw_builder.peek()) # snapshot of current state
raw_builder.set("port", 9000)
raw_config = raw_builder.build_dict()RawConfigBuilder shares the exact same loading, merging, and priority logic as ConfigBuilder — it just skips the Pydantic validation step.
Let Pydantic enforce your business rules:
from pydantic import BaseModel, Field
class AppConfig(BaseModel):
port: int = Field(default=8000, ge=1, le=65535) # Port range
workers: int = Field(default=4, ge=1, le=256) # Worker count
timeout: float = Field(default=30.0, gt=0) # Positive timeout
environment: str = Field(default="production", pattern="^(dev|staging|production)$")
# Invalid config raises validation error
config = ConfigBuilder(AppConfig).from_dict({"port": 99999}).build() # ❌ Fails validationPyConfigre implements a simple, predictable priority system. Later sources override earlier ones:
File → Environment Variables → Dictionary → Explicit .set() method
↑ ↑
Lowest Priority Highest Priority
Example:
# YAML file has: debug: false, port: 8000
# ENV var: MYAPP_DEBUG=true
# Dict: {"port": 3000}
config = (
ConfigBuilder(AppConfig)
.from_file("config.yaml") # debug=false, port=8000
.from_env("MYAPP_") # debug=true (overrides file)
.from_dict({"port": 3000}) # port=3000 (overrides both)
.build()
)
# Result: debug=true, port=3000This pattern matches how most systems expect configuration to work—sensible defaults in files, environment-specific overrides via env vars, and runtime tweaks via dictionaries.
| Format | File Extension | Dependency | Notes |
|---|---|---|---|
| YAML | .yaml, .yml |
pyyaml |
Human-readable, widely used |
| JSON | .json |
Built-in (stdlib) | Strict, universally supported |
| TOML | .toml |
tomllib (3.11+) or tomli | Clean syntax, configuration standard |
| Environment Variables | — | Built-in | Perfect for secrets and overrides |
| Python Dicts | — | Built-in | Runtime configuration |
Here's a complete, production-like example:
from pydantic import BaseModel, Field
from typing import Optional
from pyconfigre import ConfigBuilder
# Configuration schema
class DatabaseConfig(BaseModel):
host: str = "localhost"
port: int = Field(default=5432, ge=1, le=65535)
username: str
password: str
database: str
class LoggingConfig(BaseModel):
level: str = Field(default="INFO", pattern="^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$")
format: str = "json"
output: str = "stdout"
class AppConfig(BaseModel):
app_name: str = "my-api"
version: str = "1.0.0"
debug: bool = False
host: str = "0.0.0.0"
port: int = Field(default=8000, ge=1, le=65535)
database: DatabaseConfig
logging: LoggingConfig = Field(default_factory=LoggingConfig)
secret_key: Optional[str] = None
# Load configuration in your application startup
def setup_config() -> AppConfig:
"""Load and validate application configuration."""
return (
ConfigBuilder(AppConfig)
.from_file("config/default.yaml") # Defaults
.from_file(f"config/{os.getenv('ENV')}.yaml", optional=True)
.from_env("MYAPI_") # Override with env vars
.build()
)
# Usage
config = setup_config()
print(f"Starting {config.app_name} v{config.version}")
print(f"Listening on {config.host}:{config.port}")
print(f"Database: {config.database.host}")Configuration file (config/default.yaml):
app_name: my-api
version: 1.0.0
debug: false
database:
host: localhost
username: app_user
password: ${DB_PASSWORD} # Can use env vars or load separately
database: app_db
logging:
level: INFOEnvironment override (config/production.yaml):
debug: false
database:
host: prod-db.example.com
username: prod_user
logging:
level: WARNINGThis approach gives you:
- ✅ Type-safe configuration with validation
- ✅ Environment-specific configurations
- ✅ Secrets via environment variables
- ✅ Sensible defaults that developers can override locally
- ✅ Production ready
import pytest
from pydantic import BaseModel, ValidationError
from pyconfigre import ConfigBuilder
class TestConfig(BaseModel):
feature_enabled: bool = False
api_key: str
def test_config_validation():
"""Test that invalid config raises ValidationError."""
with pytest.raises(ValidationError):
ConfigBuilder(TestConfig).from_dict({}).build() # Missing required api_key
def test_config_building():
"""Test successful configuration building."""
config = (
ConfigBuilder(TestConfig)
.from_dict({"api_key": "test-key"})
.set("feature_enabled", True)
.build()
)
assert config.feature_enabled is True
assert config.api_key == "test-key"
def test_config_priority():
"""Test configuration priority (file < env < dict)."""
config = (
ConfigBuilder(TestConfig)
.from_dict({"api_key": "from-dict", "feature_enabled": False})
.from_dict({"api_key": "from-second-dict"}) # Overrides
.build()
)
assert config.api_key == "from-second-dict" # Later dict wins- Define configuration schemas early – Use Pydantic models as your configuration contract
- Use environment variables for secrets – Never hardcode API keys, passwords, or tokens
- Provide sensible defaults – Make common values optional so developers have fewer things to configure
- Fail fast on invalid config – Call
.build()early in your app startup, not lazily - Test your configuration – Write tests that verify config loading from different sources
- Version your schema – Document breaking changes to your config structure
- Use type hints everywhere – Let IDE autocomplete guide developers using your app
Never hardcode secrets:
# ❌ DON'T DO THIS
config = ConfigBuilder(AppConfig).from_dict({
"api_key": "sk-1234567890abcdef"
}).build()
# ✅ DO THIS
config = (
ConfigBuilder(AppConfig)
.from_file("config.yaml") # No secrets in file
.from_env("MYAPP_") # Get secrets from environment
.build()
)Use strong validation:
from pydantic import BaseModel, Field
class SecureConfig(BaseModel):
api_key: str = Field(min_length=32) # Enforce minimum length
timeout: float = Field(gt=0) # Must be positive
environment: str = Field(pattern="^(dev|staging|prod)$") # Whitelist values
# Invalid config is rejected before it can cause problems
config = ConfigBuilder(SecureConfig).from_env("APP_").build() # Fails fast if invalidRestrict file permissions:
# Ensure config files with sensitive data are readable only by your app
chmod 600 config/production.yaml| Aspect | Status |
|---|---|
| Version | 0.2.0 |
| Python Support | 3.10, 3.11, 3.12, 3.13, 3.14 |
| Test Coverage | ~100% (200 tests) |
| Type Checking | Full MyPy strict compliance |
| CI/CD | GitHub Actions (multi-version testing) |
Contributions are welcome! Whether it's:
- Bug reports or feature requests (open an issue)
- Documentation improvements
- Code contributions (fork and submit a PR)
Please follow the existing code style and include tests for new features.
MIT License - see LICENSE file for details.
Questions or feedback? Open an issue on GitHub.
Built with ❤️ for teams who care about clean, maintainable, type-safe Python code.