Pydantic Settings for Safe Config

Published on: July 23, 2026
Reading time: 5 minutes
Secure Python application configuration with Pydantic Settings

Application configuration looks easy when a project has only two or three values. As soon as you add databases, external APIs, development and production environments, background workers, feature flags, and tests, configuration can become a fragile collection of hard-coded strings and scattered os.getenv() calls. Pydantic Settings provides a cleaner approach: declare settings as typed fields, load values from environment variables or .env files, and validate everything when the application starts.

This guide shows how to create a central settings class, require important values, convert booleans and numbers, protect secrets, organize nested configuration, separate environments, test settings, and integrate them with FastAPI. The goal is not to add complexity. It is to make configuration predictable, documented, and safe.

Why configuration should be centralized

Scattered environment lookups create several problems. One module may treat a missing value as empty text, another may apply a default, and a third may crash much later. Ports arrive as strings, boolean values are frequently interpreted incorrectly, and invalid URLs may remain unnoticed until a request fails.

A central settings object validates the complete configuration at startup. If a required value is missing or malformed, the program fails early with a useful error. This is far better than starting successfully and failing after users begin sending requests. For the fundamentals, see our guide to reading environment variables safely in Python.

Install Pydantic Settings

Create a virtual environment and install the package:

python -m venv .venv
# Windows: .venv\Scripts\activate
# Linux/macOS: source .venv/bin/activate
pip install pydantic-settings

The package builds on Pydantic and exposes BaseSettings. Keeping dependencies inside a project-specific environment also prevents version conflicts. Review the tutorial on Python virtual environments with venv if you need a refresher.

Create your first settings class

Create config.py:

from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    app_name: str = "My API"
    debug: bool = False
    port: int = 8000
    database_url: str
    api_key: SecretStr

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
    )

settings = Settings()

The class documents every expected value and its type. database_url and api_key are required because they have no defaults. SecretStr prevents the full secret from appearing in normal object representations and accidental logs.

Create a local .env file:

APP_NAME=Academify API
DEBUG=true
PORT=8080
DATABASE_URL=sqlite:///app.db
API_KEY=local_secret

When Settings() is instantiated, Pydantic converts true into a Boolean and 8080 into an integer. If conversion is impossible, validation fails immediately.

Use prefixes to avoid collisions

Large servers often host multiple services. A prefix makes ownership clear:

model_config = SettingsConfigDict(
    env_file=".env",
    env_prefix="APP_",
    extra="ignore",
)

The field database_url will now read from APP_DATABASE_URL. Prefixes also improve documentation in deployment platforms where hundreds of environment variables may be visible.

Validate ranges and formats

Typing is only the first layer. Use constraints to reject values that are technically the right type but operationally invalid:

from pydantic import Field

class Settings(BaseSettings):
    port: int = Field(default=8000, ge=1, le=65535)
    workers: int = Field(default=1, ge=1, le=32)
    timeout_seconds: float = Field(default=10.0, gt=0)

A negative timeout or port above 65535 is rejected before the server starts. This approach fits naturally with the concepts explained in Python type hints for cleaner code.

Lists, dictionaries, and JSON values

Settings can contain structured data:

class Settings(BaseSettings):
    allowed_hosts: list[str] = ["localhost"]
    feature_flags: dict[str, bool] = {}

Provide complex values as JSON:

ALLOWED_HOSTS=["academify.com.br", "api.academify.com.br"]
FEATURE_FLAGS={"new_login": true, "cache": false}

JSON avoids ambiguous comma splitting and preserves types inside collections.

Nested configuration

As a project grows, group related values into models:

from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict

class DatabaseSettings(BaseModel):
    host: str = "localhost"
    port: int = 5432
    name: str = "app"

class Settings(BaseSettings):
    database: DatabaseSettings = DatabaseSettings()

    model_config = SettingsConfigDict(
        env_nested_delimiter="__"
    )

You can now set DATABASE__HOST and DATABASE__PORT. Nested models keep the Python interface readable while remaining compatible with standard deployment systems.

Integrate settings with FastAPI

A FastAPI application should not rebuild settings on every request. Cache the object and inject it as a dependency:

from functools import lru_cache
from fastapi import Depends, FastAPI

app = FastAPI()

@lru_cache
def get_settings() -> Settings:
    return Settings()

@app.get("/info")
def info(config: Settings = Depends(get_settings)):
    return {
        "app_name": config.app_name,
        "debug": config.debug,
    }

The cache creates one settings object per process. This pattern combines well with our guide to building APIs with FastAPI and avoids repeated file reads and validation.

Separate development, test, and production

Do not store every environment in one file. A simple local strategy selects a file using an external variable:

import os
from pydantic_settings import BaseSettings, SettingsConfigDict

ENV = os.getenv("APP_ENV", "development")

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=f".env.{ENV}",
        extra="ignore",
    )

You may keep .env.development and .env.test locally. Production secrets, however, should come from the hosting platform, container runtime, Kubernetes Secret, or a dedicated secret manager. Never commit real credentials. The same principle is useful when you run Python in Docker.

Test configuration without relying on the machine

Instantiate settings directly in tests:

def test_settings():
    config = Settings(
        database_url="sqlite:///:memory:",
        api_key="test",
        debug=True,
    )
    assert config.debug is True
    assert config.port == 8000

With pytest, monkeypatch can create temporary environment variables. This keeps tests deterministic and prevents a developer’s local values from changing results.

Common mistakes

  • Creating settings in every module: centralize construction or use caching.
  • Committing secrets: commit only a documented .env.example.
  • Using strings for everything: declare Booleans, integers, lists, URLs, and constrained fields.
  • Ignoring validation errors: startup failure is useful when configuration is invalid.
  • Logging complete settings: avoid unnecessary output even when using SecretStr.
  • Assuming the wrong priority: real environment variables should normally override local files.

Production checklist

Use consistent names, document every variable, rotate credentials, restrict permissions, validate ranges and formats, and provide secrets only at runtime. Add health checks that confirm required dependencies are reachable without exposing secret values. In CI pipelines, rely on the platform’s protected secret storage rather than plain repository variables.

The official Pydantic Settings documentation covers sources, aliases, nested models, command-line support, and customization. The SecretStr reference explains how secret values are represented and serialized.

Conclusion

Pydantic Settings turns environment variables into a typed, validated interface. Instead of spreading parsing logic and defaults across a codebase, you define one model that documents what the application needs. Invalid deployment data is discovered early, tests become easier, and sensitive values are handled more carefully.

Start with a small class, add constraints that reflect real operational limits, and separate environments as the project grows. Good configuration management is not just code organization; it is a reliability and security feature.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Desenvolvedor programando em Python com Ruff para lint e formatação
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Ruff for Python: Lint and Format Code Step by Step

    Learn Ruff for Python linting, formatting, automatic fixes, pyproject.toml, VS Code and CI with a practical project configuration.

    Ler mais

    Tempo de leitura: 9 minutos
    22/07/2026
    2 balões de comentários em um fundo laranja
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Python Comments: Best Practices and Examples

    Learn Python comments with single-line and inline examples, block explanations, TODO notes, docstrings, security and performance context, review standards, and

    Ler mais

    Tempo de leitura: 6 minutos
    10/07/2026
    Pessoa programando em um notebook, vista de trás, com código desfocado exibido na tela em um fundo escuro
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Python Docstrings: Document Code Clearly

    Learn Python docstrings for functions, classes, methods, modules, parameters, returns, exceptions, examples, pydoc, conventions, and documentation tools.

    Ler mais

    Tempo de leitura: 6 minutos
    10/07/2026
    Logo do Python com o texto 'PEP 8' sobre fundo azul escuro, representando o guia de estilo da linguagem
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    PEP 8 in Python: Write Cleaner Code

    Learn PEP 8 in Python with naming, indentation, line length, imports, whitespace, comments, functions, classes, tools, exceptions, and practical refactoring.

    Ler mais

    Tempo de leitura: 6 minutos
    10/07/2026
    Configuração de logs em aplicações Python usando logging
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Python Logging: A Complete Beginner’s Guide

    Learn Python logging from scratch: levels, files, formatters, exceptions, handlers, rotation, and practical examples for reliable applications.

    Ler mais

    Tempo de leitura: 10 minutos
    10/07/2026
    Dicas para otimizar scripts Python lentos
    Best Practices
    Foto de perfil de Leandro Hirt da Academify

    Why Is Your Python Script Slow? Fixes

    Learn why Python scripts run slowly and how to fix it: use built-ins, list comprehensions, sets, generators, NumPy, cProfile profiling,

    Ler mais

    Tempo de leitura: 3 minutos
    03/06/2026