Configuration - Environment Management
The Configuration module provides type-safe, environment-based settings management using Pydantic. It automatically loads configuration from .env files and environment variables.
Overview
The Configuration module offers:
- Type safety - Pydantic validation ensures correct types
- Auto-loading - Reads
.env and .env.local files automatically
- Environment detection - Distinguish between local and production
- FastAPI integration - Ready-to-use FastAPI configuration
- Easy extension - Add custom settings by extending the base class
Basic Usage
Loading Settings
| from tools.config import Settings
# Automatically loads from .env and .env.local
settings = Settings()
# Access configuration values
debug_mode = settings.debug
api_prefix = settings.api_prefix_v1
is_local = settings.IS_LOCAL
|
Environment Files
The module loads configuration from two files in order:
.env - Base configuration (committed to git)
.env.local - Local overrides (not committed, in .gitignore)
.env (shared configuration):
| DEBUG=false
TITLE=My Application
API_PREFIX_V1=/api/v1
|
.env.local (local overrides):
Available Settings
Default Configuration Fields
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | class Settings(BaseSettings):
# Environment detection
IS_LOCAL: bool = False
# FastAPI settings
debug: bool = False
title: str = "FastAPI"
summary: str | None = None
description: str = ""
version: str = "0.1.0"
openapi_url: str = "/openapi.json"
docs_url: str = "/docs"
redoc_url: str = "/redoc"
openapi_prefix: str = ""
# API configuration
api_prefix_v1: str = "/api/v1"
# CORS settings
allowed_hosts: list[str] = ["*"]
|
FastAPI Integration
Basic FastAPI Setup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | from fastapi import FastAPI
from tools.config import Settings
settings = Settings()
# Use fastapi_kwargs property for clean initialization
app = FastAPI(**settings.fastapi_kwargs)
@app.get("/")
async def root():
return {
"title": settings.title,
"version": settings.version,
"environment": "local" if settings.IS_LOCAL else "production"
}
|
FastAPI with Custom Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from tools.config import Settings
settings = Settings()
app = FastAPI(**settings.fastapi_kwargs)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_hosts,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
|
Extending Settings
Adding Custom Fields
Extend the Settings class to add your own configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13 | from tools.config import Settings as BaseSettings
class Settings(BaseSettings):
# Add your custom fields
DATABASE_URL: str = "sqlite:///./app.db"
SECRET_KEY: str
REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6379
# Add validation
@property
def redis_url(self) -> str:
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}"
|
Then in your .env file:
| DATABASE_URL=postgresql://user:pass@localhost/dbname
SECRET_KEY=your-secret-key-here
REDIS_HOST=redis.example.com
REDIS_PORT=6380
|
Database Configuration Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | from pydantic import PostgresDsn
from tools.config import Settings as BaseSettings
class DatabaseSettings(BaseSettings):
POSTGRES_SERVER: str = "localhost"
POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str
POSTGRES_DB: str = "app"
POSTGRES_PORT: int = 5432
@property
def database_url(self) -> str:
return (
f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
f"@{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
)
# Usage
settings = DatabaseSettings()
engine = create_engine(settings.database_url)
|
Environment-Based Behavior
Conditional Logic
Use IS_LOCAL to change behavior based on environment:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | from tools.config import Settings
from tools.logger import Logger, LogType
settings = Settings()
# Select log type based on environment
logger = Logger(
__name__,
log_type=LogType.LOCAL if settings.IS_LOCAL else LogType.GOOGLE_CLOUD
)
# Configure database
if settings.IS_LOCAL:
DATABASE_URL = "sqlite:///./dev.db"
else:
DATABASE_URL = settings.production_database_url
# Debug mode
app = FastAPI(
**settings.fastapi_kwargs,
debug=settings.debug
)
|
Complete Example
Here's a complete example of a FastAPI application with extended configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 | from fastapi import FastAPI, Depends
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from tools.config import Settings as BaseSettings
from tools.logger import Logger, LogType
class AppSettings(BaseSettings):
# Database
DATABASE_URL: str = "sqlite:///./app.db"
# Security
SECRET_KEY: str
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# External APIs
EXTERNAL_API_KEY: str = ""
EXTERNAL_API_URL: str = "https://api.example.com"
# Initialize
settings = AppSettings()
logger = Logger(
__name__,
log_type=LogType.LOCAL if settings.IS_LOCAL else LogType.GOOGLE_CLOUD
)
# Database
engine = create_engine(settings.DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
# FastAPI
app = FastAPI(**settings.fastapi_kwargs)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.on_event("startup")
async def startup():
logger.info(f"Starting application: {settings.title}")
logger.info(f"Environment: {'local' if settings.IS_LOCAL else 'production'}")
logger.debug(f"Database: {settings.DATABASE_URL}")
@app.get("/health")
async def health_check():
return {
"status": "healthy",
"version": settings.version,
"environment": "local" if settings.IS_LOCAL else "production"
}
|
Best Practices
1. Never Commit Secrets
Always keep sensitive data in .env.local:
| # .gitignore (should already include this)
.env.local
|
2. Provide Defaults
Set sensible defaults for non-sensitive configuration:
| class Settings(BaseSettings):
DATABASE_URL: str = "sqlite:///./dev.db" # Safe default
SECRET_KEY: str # No default - must be provided
|
3. Use Type Annotations
Leverage Pydantic's type validation:
| class Settings(BaseSettings):
PORT: int = 8000 # Validates as integer
DEBUG: bool = False # Validates as boolean
ALLOWED_HOSTS: list[str] = ["*"] # Validates as list
|
4. Document Your Settings
Add docstrings to custom settings:
| class Settings(BaseSettings):
"""Application configuration.
Loads from .env and .env.local files.
"""
DATABASE_URL: str = "sqlite:///./app.db"
"""Database connection string."""
SECRET_KEY: str
"""Secret key for JWT tokens. Must be set in .env.local."""
|
Use Pydantic validators for complex validation:
1
2
3
4
5
6
7
8
9
10
11
12 | from pydantic import field_validator
class Settings(BaseSettings):
MIN_VALUE: int = 0
MAX_VALUE: int = 100
@field_validator('MAX_VALUE')
@classmethod
def validate_max_greater_than_min(cls, v, info):
if 'MIN_VALUE' in info.data and v <= info.data['MIN_VALUE']:
raise ValueError('MAX_VALUE must be greater than MIN_VALUE')
return v
|
Testing
Testing with Override
Override settings in tests:
1
2
3
4
5
6
7
8
9
10
11
12 | import pytest
from tools.config import Settings
def test_with_custom_settings():
settings = Settings(
DEBUG=True,
IS_LOCAL=True,
DATABASE_URL="sqlite:///:memory:"
)
assert settings.DEBUG is True
assert settings.IS_LOCAL is True
|
Testing Environment Loading
| import os
from tools.config import Settings
def test_env_loading(monkeypatch):
monkeypatch.setenv("DEBUG", "true")
monkeypatch.setenv("TITLE", "Test App")
settings = Settings()
assert settings.debug is True
assert settings.title == "Test App"
|