FastAPI

FastAPI Pydantic Settings — Environment Configuration Management

Thirdy Gayares
15 min read

🎓 What You Will Learn

  • Configuration Management: Why hardcoding settings is dangerous
  • Pydantic Settings: Using BaseSettings for configuration
  • Environment Variables: Loading from .env files
  • Validation: Type checking and validation of settings
  • Secrets: Managing sensitive data securely
  • Multiple Environments: Development, staging, production configs
ConfigurationPydanticEnvironmentSecrets

1Why Configuration Management Matters

Hardcoding configuration (database URLs, API keys, credentials) is dangerous. It exposes secrets and makes it impossible to run the same code in different environments. Pydantic Settings provides a clean, type-safe way to manage configuration.

Configuration should never be hardcoded. Different environments (development, staging, production) need different settings. Use environment variables to inject configuration at runtime.

❌ Hardcoded (Bad)

app = FastAPI() @app.get("/") async def root(): DATABASE_URL = "postgresql://user:password@localhost/db" API_KEY = "secret123456" return {"db": DATABASE_URL}
Secrets exposed in code
Hard to change per environment
Can commit secrets to git

2Pydantic Settings Basics

Pydantic Settings extends Pydantic to load configuration from environment variables and .env files. Install it first.

bash
pip install pydantic-settings
app/config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    # Required settings (no default value)
    database_url: str
    api_key: str

    # Optional settings with defaults
    debug: bool = False
    log_level: str = "INFO"
    port: int = 8000

    class Config:
        env_file = ".env"  # Load from .env file

# Create global settings instance
settings = Settings()

3Loading from .env Files

Create a .env file in your project root with environment variables. Pydantic Settings loads them automatically.

.env
DATABASE_URL=postgresql://user:password@localhost/mydb
API_KEY=sk_live_1234567890
DEBUG=false
LOG_LEVEL=INFO
PORT=8000
SECRET_KEY=your-secret-key-here

Security Warning: Never commit .env to version control. Add it to .gitignore.

.gitignore
# Ignore environment files
.env
.env.local
.env.*.local
.env.prod

# Ignore IDE
.vscode/
.idea/

# Ignore Python
__pycache__/
*.pyc
.venv/
venv/

4Type Validation & Defaults

Pydantic validates types automatically. If you set port: int = 8000, Pydantic ensures the value is an integer.

app/config.py
from pydantic_settings import BaseSettings
from typing import Optional

class Settings(BaseSettings):
    # Required string
    database_url: str

    # Optional string
    redis_url: Optional[str] = None

    # Integer with default
    port: int = 8000

    # Boolean with default
    debug: bool = False

    # List of strings
    allowed_hosts: list[str] = ["localhost", "127.0.0.1"]

    class Config:
        env_file = ".env"

settings = Settings()

# Usage
print(settings.port)  # 8000 (int)
print(settings.debug)  # False (bool)
print(settings.allowed_hosts)  # ["localhost", "127.0.0.1"] (list)

Type Hints: Use type hints for automatic validation. Optional[str] allows None, str requires a value.

5Integrating with FastAPI

Create a settings module and use it throughout your FastAPI application.

app/config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    app_name: str = "My API"
    database_url: str
    api_key: str
    debug: bool = False
    log_level: str = "INFO"

    class Config:
        env_file = ".env"
        case_sensitive = False

settings = Settings()
app/main.py
from fastapi import FastAPI
from app.config import settings
from sqlalchemy import create_engine

# Use settings in your FastAPI app
app = FastAPI(title=settings.app_name, debug=settings.debug)

# Create database engine with configured URL
engine = create_engine(settings.database_url)

@app.get("/")
async def root():
    return {
        "app_name": settings.app_name,
        "debug": settings.debug,
        "log_level": settings.log_level
    }

@app.get("/config")
async def get_config():
    return {
        "database_url": settings.database_url[:20] + "...",  # Hide password
        "api_key": "hidden",
        "debug": settings.debug
    }

6Environment-Specific Settings

Support multiple environments (development, staging, production) by using environment variables to control behavior.

app/config.py
import os
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    environment: str = os.getenv("ENV", "development")
    database_url: str
    api_key: str

    # Development vs Production
    debug: bool = environment == "development"
    log_level: str = "DEBUG" if environment == "development" else "INFO"

    # Database settings
    db_pool_size: int = 5
    db_max_overflow: int = 10

    # Security
    cors_origins: list[str] = ["http://localhost:3000"]

    class Config:
        env_file = ".env"

    def __init__(self, **data):
        super().__init__(**data)
        # Load environment-specific .env file
        if self.environment == "production":
            self.cors_origins = ["https://example.com", "https://www.example.com"]
            self.db_pool_size = 20

settings = Settings()
.env.development
ENV=development
DATABASE_URL=postgresql://user:password@localhost/mydb_dev
API_KEY=dev_key_12345
DEBUG=true
LOG_LEVEL=DEBUG
CORS_ORIGINS=["http://localhost:3000", "http://localhost:3001"]
.env.production
ENV=production
DATABASE_URL=postgresql://user:[email protected]/mydb
API_KEY=sk_live_actual_key_here
DEBUG=false
LOG_LEVEL=INFO
CORS_ORIGINS=["https://example.com", "https://www.example.com"]

7Custom Validation

Validate settings using field validators or custom logic.

app/config.py
from pydantic_settings import BaseSettings
from pydantic import field_validator

class Settings(BaseSettings):
    database_url: str
    api_key: str
    port: int = 8000

    class Config:
        env_file = ".env"

    @field_validator("database_url")
    @classmethod
    def validate_database_url(cls, v):
        if not v.startswith(("postgresql://", "mysql://", "sqlite://")):
            raise ValueError("Invalid database URL format")
        return v

    @field_validator("api_key")
    @classmethod
    def validate_api_key(cls, v):
        if len(v) < 20:
            raise ValueError("API key must be at least 20 characters")
        return v

    @field_validator("port")
    @classmethod
    def validate_port(cls, v):
        if not 1 <= v <= 65535:
            raise ValueError("Port must be between 1 and 65535")
        return v

settings = Settings()

8Secrets Management with .env

For sensitive data, use .env files that are never committed to git. In production, use environment variables or secret management systems.

app/config.py
from pydantic_settings import BaseSettings
from typing import Optional

class Settings(BaseSettings):
    # Secrets (from .env or environment)
    database_password: str
    api_key: str
    jwt_secret: str
    aws_access_key: Optional[str] = None
    aws_secret_key: Optional[str] = None

    # Non-secrets (can be in git)
    app_name: str = "My API"
    database_host: str = "localhost"
    debug: bool = False

    class Config:
        env_file = ".env"
        # Warn if env_file is missing
        case_sensitive = True

settings = Settings()

Production Secrets: Use AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, or similar for production. Never rely on .env files in production.

9Nested Configuration Objects

For complex configurations, use nested Pydantic models to organize settings.

app/config.py
from pydantic_settings import BaseSettings
from pydantic import BaseModel
from typing import Optional

class DatabaseConfig(BaseModel):
    url: str
    pool_size: int = 5
    max_overflow: int = 10
    echo: bool = False

class CORSConfig(BaseModel):
    origins: list[str] = ["localhost"]
    allow_credentials: bool = True
    allow_methods: list[str] = ["*"]
    allow_headers: list[str] = ["*"]

class Settings(BaseSettings):
    app_name: str = "My API"
    debug: bool = False
    database: DatabaseConfig
    cors: CORSConfig

    class Config:
        env_file = ".env"
        env_nested_delimiter = "__"

# .env file
# DATABASE__URL=postgresql://user:pass@localhost/db
# DATABASE__POOL_SIZE=10
# CORS__ORIGINS=["https://example.com"]
# CORS__ALLOW_CREDENTIALS=true

settings = Settings()
print(settings.database.url)  # postgresql://user:pass@localhost/db
print(settings.cors.origins)  # ["https://example.com"]

10Testing with Different Settings

Override settings in tests to avoid using production credentials.

tests/test_config.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.config import settings

@pytest.fixture
def test_client():
    # Create test client with test settings
    with TestClient(app) as client:
        yield client

def test_app_with_test_settings(monkeypatch):
    # Override settings for this test
    monkeypatch.setattr(settings, "debug", True)
    monkeypatch.setattr(settings, "database_url", "sqlite:///:memory:")

    assert settings.debug is True
    assert "sqlite" in settings.database_url

def test_config_loaded():
    # Test that settings loaded correctly
    assert settings.app_name is not None
    assert settings.database_url is not None
    assert settings.port > 0

11Common Configuration Patterns

PatternUse CaseExample
Environment-based configDifferent settings per environmentENV=production DATABASE_URL=...
Nested modelsOrganize complex configurationsdatabase.url, cors.origins
Custom validatorsValidate sensitive settingsEnsure API key format
Optional secretsSome features optionalAWS_KEY optional in development
Derived settingsCalculate from other settingsdebug = (environment == development)

12Configuration Best Practices

  • Never commit .env to version control
  • Use type hints for all settings
  • Set reasonable defaults for optional settings
  • Validate sensitive settings (API keys, URLs)
  • Use environment-specific .env files (.env.production, .env.development)
  • Hide sensitive data when logging settings
  • Use secret management systems in production
  • Document all required settings in README
  • Test with different configurations
  • Override settings in tests to avoid side effects

13Production Configuration Setup

In production, load settings from environment variables or secret management services.

app/config.py
import os
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    app_name: str = "Production API"
    environment: str = os.getenv("ENV", "production")
    database_url: str  # Required in production
    api_key: str  # Required in production
    jwt_secret: str  # Required in production
    aws_access_key: str = os.getenv("AWS_ACCESS_KEY_ID", "")
    log_level: str = "INFO"

    class Config:
        env_file = ".env"  # Will be empty in production
        case_sensitive = True

    def __post_init__(self):
        # Validate required settings in production
        if self.environment == "production":
            required = ["database_url", "api_key", "jwt_secret"]
            for field in required:
                if not getattr(self, field):
                    raise ValueError(f"{field} must be set in production")

settings = Settings()

Docker & Environment Variables: In Docker/Kubernetes, pass secrets as environment variables or use a secret manager. The application loads them at startup.

14Advanced Configuration Patterns

app/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache

class Settings(BaseSettings):
    database_url: str
    api_key: str
    debug: bool = False

    class Config:
        env_file = ".env"

# Cache settings instance
@lru_cache()
def get_settings():
    return Settings()

# Usage in FastAPI
from fastapi import Depends

async def some_endpoint(settings: Settings = Depends(get_settings)):
    return {"debug": settings.debug}

15Summary & Next Steps

Master Pydantic Settings to build secure, flexible FastAPI applications that work across development, staging, and production environments.

🚀 Congratulations! You now understand how to manage configuration securely in FastAPI using Pydantic Settings. Build with confidence across all environments!

About the Author

TG

Thirdy Gayares

Passionate developer creating custom solutions for everyone. I specialize in building user-friendly tools that solve real-world problems while maintaining the highest standards of security and privacy.