Sillo provides type-safe configuration management using Pydantic, with .env
loading, validation, and environment-specific settings.
Overview
Section titled “Overview”The configuration system provides:
- Type Safety - Full type hints, IDE autocomplete, mypy support
- Validation - Pydantic validates all values on load
- Environment Variables - Load from
.envfiles - Required vs Optional - Clear which settings are mandatory
- Secret Masking - Automatically hides sensitive values
- Environment-Specific - Different configs per environment (dev/staging/prod)
- Defaults - Sensible defaults for optional settings
Installation
Section titled “Installation”Nothing to install. Sillo parses .env itself — python-dotenv is not a
dependency, and there is no load_dotenv() call to remember. Constructing a
config, or a SilloApp, loads the project’s .env on its own. See
Environment & .env for the file format and the
precedence rules.
Quick Start
Section titled “Quick Start”1. Define Configuration
Section titled “1. Define Configuration”from sillo.config import Config, Fieldfrom typing import Literal
class AppConfig(Config): """Application configuration from .env"""
# Required fields database_url: str jwt_secret: str
# Optional fields with defaults debug: bool = False log_level: Literal['debug', 'info', 'warning', 'error'] = 'info' port: int = 8000
# Load configuration — .env is found and read automaticallyconfig = AppConfig()2. Create .env File
Section titled “2. Create .env File”DATABASE_URL=postgresql://localhost/mydbJWT_SECRET=your-secret-keyDEBUG=trueLOG_LEVEL=debugPORT=80003. Use in Application
Section titled “3. Use in Application”from sillo import SilloApp
app = SilloApp( debug=config.debug, title="My API")
@app.on_startupasync def startup(): print(f"Starting in {config.environment}") await database.connect(config.database_url)Configuration Classes
Section titled “Configuration Classes”Basic Structure
Section titled “Basic Structure”from sillo.config import Config, Fieldfrom typing import Literal, Optional
class AppConfig(Config): """Define all application settings."""
# Required string database_url: str
# Required with description api_key: str = Field(..., description="Third-party API key")
# Optional with default debug: bool = False
# Enum-like with type environment: Literal['dev', 'staging', 'prod'] = 'dev'
# Integer with default port: int = 8000
# Optional (can be None) cache_url: Optional[str] = NoneAdjusting the Defaults
Section titled “Adjusting the Defaults”An inner Env class changes how the fields map onto the environment. Every
setting is optional:
class DatabaseConfig(Config): url: str pool_size: int = 10
class Env: env_file = '.env.production' # None loads no file at all env_prefix = 'DATABASE_' # DATABASE_URL, DATABASE_POOL_SIZE case_sensitive = False # the default: field names uppercasedenv_prefix is what lets one .env serve several subsystems without the
field names growing prefixes of their own.
Type Support
Section titled “Type Support”Pydantic supports many types automatically:
from sillo.config import Configfrom typing import Literal, Optionalfrom pathlib import Path
class Config(Config): # Basic types name: str port: int timeout: float enabled: bool
# Enums environment: Literal['dev', 'staging', 'prod'] log_level: Literal['debug', 'info', 'warning', 'error']
# Optional optional_key: Optional[str] = None
# Collections allowed_origins: list[str] = [] cors_headers: dict[str, str] = {}
# Paths log_file: Path = Path('app.log')Validation
Section titled “Validation”Pydantic validates types on load:
port: int = 8000 # Must be integer or convertible to intdebug: bool = False # Must be bool or 'true'/'false' stringtimeout: float = 30.0 # Must be float
# These work:PORT=8000 # Loaded as intDEBUG=true # Converted to boolDEBUG=yes # So are yes/no, on/off and 1/0TIMEOUT=30.5 # Loaded as float
# These fail:PORT=eight # ValidationErrorDEBUG=maybe # ValidationError.env Files
Section titled “.env Files”File Format
Section titled “File Format”# Simple key=valueDATABASE_URL=postgresql://localhost/dbDEBUG=truePORT=8000
# Spaces around = are fineKEY = value
# Quotes keep whitespace; single quotes stop all expansionQUOTED=" spaces kept "LITERAL='nothing $expands here'
# Multi-line values, for keys and certificatesPRIVATE_KEY="""-----BEGIN PRIVATE KEY-----MIIEvQIBADANBgkq...-----END PRIVATE KEY-----"""
# References to earlier keys, and to the surrounding environmentDB_HOST=db.internalDATABASE_URL=postgres://${DB_HOST}:5432/appPORT=${PORT:-8000}
# Comments with ## This is a commentSECRET_KEY=abc123 # Inline comment — the space before # is what makes it onePASSWORD=pa#ssword # No space, so this hash is part of the passwordThe full grammar is in Environment & .env.
File Locations
Section titled “File Locations”project/├── .env # Main config (git ignored)├── .env.example # Template (commit this)├── .env.development # Development overrides├── .env.production # Production config (git ignored)└── .env.test # Test configEnvironment-Specific Loading
Section titled “Environment-Specific Loading”The usual way is from the outside, so the code does not have to know:
SILLO_ENV_FILE=.env.production uvicorn app:appOr decide in code:
import os
class AppConfig(Config): database_url: str
class Env: env_file = f".env.{os.getenv('ENVIRONMENT', 'development')}"To layer a developer’s overrides on top of a shared file:
from sillo.env import load_env
load_env() # .envload_env('.env.local', override=True) # then whatever this machine changesUsage Patterns
Section titled “Usage Patterns”Basic Usage
Section titled “Basic Usage”# Load configconfig = AppConfig()
# Access values (type-safe)db_url: str = config.database_urlport: int = config.portdebug: bool = config.debug
# All have proper types and IDE autocompleteWith Sillo App
Section titled “With Sillo App”from sillo import SilloApp
config = AppConfig()
app = SilloApp( debug=config.debug, title="My API",)
@app.on_startupasync def startup(): print(f"Starting {app.title}") print(f"Environment: {config.environment}") print(f"Database: {config.database_url}")Secret Masking
Section titled “Secret Masking”config = AppConfig()
# JWT_SECRET, API_KEY, PASSWORD fields are automatically maskedprint(config)# <AppConfig {..., 'jwt_secret': '***', 'api_key': '***', ...}>
# Secrets not masked in direct accesssecret = config.jwt_secret # Full value available in codeConditional Behavior
Section titled “Conditional Behavior”class AppConfig(Config): environment: Literal['development', 'production'] debug: bool = False
config = AppConfig()
# Change behavior based on configif config.debug: logger.setLevel('DEBUG')else: logger.setLevel('INFO')
if config.environment == 'production': use_https()else: allow_http()Feature Flags
Section titled “Feature Flags”class AppConfig(Config): enable_payments: bool = False enable_notifications: bool = False enable_analytics: bool = False
config = AppConfig()
# Enable/disable features via .envif config.enable_payments: setup_stripe(config.stripe_api_key)
if config.enable_notifications: setup_email_service()Common Patterns
Section titled “Common Patterns”Database Configuration
Section titled “Database Configuration”class AppConfig(Config): # Option 1: Full URL database_url: str
# Option 2: Components database_host: str = 'localhost' database_port: int = 5432 database_user: str database_password: str database_name: str
@property def db_connection_string(self) -> str: """Build connection string from components.""" return ( f"postgresql://" f"{self.database_user}:{self.database_password}" f"@{self.database_host}:{self.database_port}" f"/{self.database_name}" )Security Settings
Section titled “Security Settings”class AppConfig(Config): jwt_secret: str # Never commit! jwt_algorithm: str = 'HS256' jwt_expiration_hours: int = 24
cors_origins: str = '*' # Restrict in production enable_swagger: bool = False # Disable in production debug: bool = False # Always False in production
# Production .envDEBUG=falseENABLE_SWAGGER=falseCORS_ORIGINS=https://app.example.com,https://www.example.comEmail Configuration
Section titled “Email Configuration”from typing import Literal
class EmailConfig(Config): email_provider: Literal['smtp', 'sendgrid', 'mailgun'] = 'smtp'
# SMTP settings smtp_host: str = 'localhost' smtp_port: int = 587 smtp_user: str = '' smtp_password: str = ''
# API key for SendGrid/Mailgun api_key: str = ''
email_from: str = 'noreply@example.com'
# Use in appconfig = EmailConfig()
if config.email_provider == 'smtp': send_via_smtp(config)elif config.email_provider == 'sendgrid': send_via_sendgrid(config)Logging Configuration
Section titled “Logging Configuration”from typing import Literal
class LoggingConfig(Config): log_level: Literal['debug', 'info', 'warning', 'error'] = 'info' log_format: Literal['json', 'text', 'pretty'] = 'json' log_file: str = 'app.log' log_max_size_mb: int = 10 log_backup_count: int = 5Validation
Section titled “Validation”Required Fields
Section titled “Required Fields”class AppConfig(Config): # These MUST be in .env or will raise ValidationError database_url: str jwt_secret: str api_key: strIf missing:
ValidationError: 3 validation errors for AppConfigdatabase_url Field required (type=missing)jwt_secret Field required (type=missing)api_key Field required (type=missing)Type Validation
Section titled “Type Validation”class Config(Config): port: int = 8000 timeout: float = 30.0 debug: bool = False
# .envPORT=8000 # OK: Valid integerTIMEOUT=30.5 # OK: Valid floatDEBUG=true # OK: Valid boolean
PORT=not_a_number # ValidationError: Invalid integerTIMEOUT=abc # ValidationError: Invalid floatDEBUG=yes # ValidationError: Invalid boolean (only true/false)Enum Validation
Section titled “Enum Validation”class Config(Config): environment: Literal['dev', 'staging', 'prod']
ENVIRONMENT=dev # OKENVIRONMENT=prod # OKENVIRONMENT=test # ValidationError: Invalid literal
# Error:ValidationError: 1 validation error for Configenvironment Input should be 'dev', 'staging' or 'prod'Environment Setup
Section titled “Environment Setup”Development
Section titled “Development”DEBUG=trueLOG_LEVEL=debugDATABASE_URL=sqlite:///dev.dbJWT_SECRET=dev-secret-not-secureENABLE_SWAGGER=trueCORS_ORIGINS=*Staging
Section titled “Staging”DEBUG=falseLOG_LEVEL=infoDATABASE_URL=postgresql://staging.db.local/appJWT_SECRET=staging-secret-keyENABLE_SWAGGER=trueCORS_ORIGINS=https://staging.example.comProduction
Section titled “Production”# .env.production (never commit!)DEBUG=falseLOG_LEVEL=warningDATABASE_URL=postgresql://prod.db.aws.com/appJWT_SECRET=<generate-strong-key>ENABLE_SWAGGER=falseCORS_ORIGINS=https://app.example.com,https://www.example.comBest Practices
Section titled “Best Practices”1. Use .env.example as Template
Section titled “1. Use .env.example as Template”# Create and commit templatecp .env.example .env
# Add to .gitignoreecho ".env" >> .gitignoreecho ".env.production" >> .gitignore
# Developers copy templatecp .env.example .env# Then edit with their local values2. Define Config Once
Section titled “2. Define Config Once”from sillo.config import Config
class AppConfig(Config): database_url: str jwt_secret: str debug: bool = False
config = AppConfig()
# Use throughout app# from config import config3. Validate at Startup
Section titled “3. Validate at Startup”try: config = AppConfig()except ValidationError as e: print(f"Invalid configuration: {e}") sys.exit(1)
# Proceed only if config is validapp = SilloApp(debug=config.debug)4. Document All Settings
Section titled “4. Document All Settings”class AppConfig(Config): """Application configuration.
Load from .env file. See .env.example for template. """
database_url: str = Field( ..., description="PostgreSQL connection string" )
jwt_secret: str = Field( ..., description="Secret key for JWT signing" )
debug: bool = Field( default=False, description="Enable debug mode (never in production)" )5. Use Type Hints Everywhere
Section titled “5. Use Type Hints Everywhere”# Gooddb_url: str = config.database_urlport: int = config.port
# Avoiddb_url = config.database_url # Type not clear6. Never Commit Secrets
Section titled “6. Never Commit Secrets”.env.env.production.env.*.local
# OK to commit.env.exampleTroubleshooting
Section titled “Troubleshooting”ValidationError: Field required
Section titled “ValidationError: Field required”Problem: Required field missing from .env
Solution: Add the field to .env file
class AppConfig(Config): database_url: str # Required
# .env file must have:DATABASE_URL=postgresql://localhost/dbValidationError: Input should be a valid integer
Section titled “ValidationError: Input should be a valid integer”Problem: Non-integer value for integer field
PORT=eight # Wrong
# Fix:PORT=8000 # RightConfig not loading from .env
Section titled “Config not loading from .env”Problem: The file is not where the search looks, or the variable is already set in the real environment (which wins).
Check what the search finds, and what the file actually parses to:
from sillo.env import find_env, parse_env
print(find_env()) # the file being used, or Noneprint(parse_env(open('.env').read())) # what it parses toThe search runs upward from the working directory and stops at the project
root, so a .env above the project is deliberately ignored. Name the file
outright when it lives somewhere else:
from pathlib import Path
class AppConfig(Config): database_url: str
class Env: env_file = str(Path(__file__).parent / '.env')If the value is stale rather than missing, something exported it before the
process started — echo $DATABASE_URL — and the real environment beats the
file by design. Use load_env(..., override=True) to reverse that.
Secrets appear in logs
Section titled “Secrets appear in logs”Problem: Sensitive values logged
Solution: Config automatically masks common secret fields
# These are automatically masked:jwt_secret: str # Appears as ***api_key: str # Appears as ***password: str # Appears as ***access_token: str # Appears as ***See Also
Section titled “See Also”- Environment & .env - The file format, precedence, and
sillo.env - Security - Security best practices
- Deployment - Deployment configuration
- Pydantic Docs - Full Pydantic reference