
¿Por qué gestionar la configuración con pydantic-settings?
La configuración de una aplicación incluye todo lo que puede variar entre entornos: URLs de base de datos, claves secretas, nombres de buckets de almacenamiento, credenciales de servicios externos, flags de funcionalidades, etc.
La mejor práctica (uno de los principios de la metodología 12-Factor App) es almacenar la configuración en variables de entorno, separándola completamente del código fuente. Esto permite que el mismo código funcione en desarrollo, staging y producción sin modificaciones.
pydantic-settings lleva esta práctica un paso más allá: define la configuración como un modelo Pydantic tipado que:
- Carga los valores desde variables de entorno o archivos
.env - Válida los tipos automáticamente (una URL incorrecta falla en el arranque)
- Lanza errores claros si falta una variable obligatoria
- Soporta valores por defecto para desarrollo
Instalación
pip install pydantic-settings
pydantic-settings es un paquete separado de Pydantic desde la versión 2.
Definir el modelo de configuración
# app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import AnyHttpUrl, field_validator
from typing import Annotated
class Configuracion(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env", # Cargar desde .env si existe
env_file_encoding="utf-8",
case_sensitive=False, # DATABASE_URL y database_url son lo mismo
)
# Nombre de la aplicación
app_name: str = "Mi API FastAPI"
app_version: str = "1.0.0"
# Base de datos (obligatoria, sin valor por defecto)
database_url: str
# Seguridad (obligatoria en producción)
secret_key: str
algorithm: str = "HS256"
access_token_expire_minutes: int = 30
# Entorno de ejecución
environment: str = "desarrollo"
debug: bool = False
# Configuración del servidor
host: str = "0.0.0.0"
port: int = 8000
# CORS
allowed_origins: list[str] = ["http://localhost:3000"]
@field_validator("environment")
@classmethod
def validar_entorno(cls, v: str) -> str:
entornos_validos = {"desarrollo", "staging", "produccion"}
if v not in entornos_validos:
raise ValueError(f"El entorno debe ser uno de: {entornos_validos}")
return v
Archivo .env
El archivo .env almacena los valores de configuración para el entorno local:
# .env (no incluir en el repositorio Git)
DATABASE_URL=postgresql://usuario:contrasena@localhost/mi_bd
SECRET_KEY=mi-clave-secreta-muy-segura-y-larga-123456789
ENVIRONMENT=desarrollo
DEBUG=true
ALLOWED_ORIGINS=["http://localhost:3000","http://localhost:5173"]
Añade .env al archivo .gitignore para no incluirlo accidentalmente en el repositorio:
echo ".env" >> .gitignore
Para el repositorio, puedes incluir un .env.example con las variables necesarias pero sin valores reales:
# .env.example (sí se incluye en el repositorio)
DATABASE_URL=postgresql://usuario:contrasena@localhost/mi_bd
SECRET_KEY=cambia-esto-por-una-clave-secreta-real
ENVIRONMENT=desarrollo
DEBUG=false
ALLOWED_ORIGINS=["http://localhost:3000"]
Usar la configuración en FastAPI
La forma recomendada de integrar la configuración en FastAPI es mediante una dependencia con caché:
# app/config.py (ampliado)
from functools import lru_cache
@lru_cache
def get_settings() -> Configuracion:
"""Carga la configuración una sola vez y la cachea."""
return Configuracion()
# app/main.py
from fastapi import FastAPI, Depends
from app.config import Configuracion, get_settings
def create_app(settings: Configuracion) -> FastAPI:
"""Factory de la aplicación que recibe la configuración."""
app = FastAPI(
title=settings.app_name,
version=settings.app_version,
debug=settings.debug,
)
# Configurar CORS con los orígenes de la configuración
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
return app
settings = get_settings()
app = create_app(settings)
# Endpoint que expone información de configuración pública
@app.get("/info")
async def info_aplicacion(config: Configuracion = Depends(get_settings)):
return {
"nombre": config.app_name,
"version": config.app_version,
"entorno": config.environment,
}
El uso de @lru_cache garantiza que el archivo .env se lee y válida una sola vez al arrancar la aplicación. Las llamadas posteriores a get_settings() devuelven la misma instancia cacheada.
Configuraciones específicas por entorno
Para entornos distintos, puedes heredar de la configuración base:
# app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class ConfiguracionBase(BaseSettings):
app_name: str = "Mi API"
database_url: str
secret_key: str
debug: bool = False
allowed_origins: list[str] = []
class ConfiguracionDesarrollo(ConfiguracionBase):
model_config = SettingsConfigDict(env_file=".env.development")
debug: bool = True
database_url: str = "sqlite:///./desarrollo.db"
secret_key: str = "dev-secret-key-no-usar-en-produccion"
allowed_origins: list[str] = [
"http://localhost:3000",
"http://localhost:5173",
]
class ConfiguracionProduccion(ConfiguracionBase):
model_config = SettingsConfigDict(env_file=".env.production")
debug: bool = False
# Estos valores DEBEN venir de variables de entorno reales
import os
from functools import lru_cache
@lru_cache
def get_settings():
entorno = os.getenv("ENVIRONMENT", "desarrollo")
if entorno == "produccion":
return ConfiguracionProduccion()
return ConfiguracionDesarrollo()
Inyectar configuración en endpoints
La configuración se puede inyectar como una dependencia normal en cualquier endpoint:
from fastapi import FastAPI, Depends
from app.config import Configuracion, get_settings
app = FastAPI()
@app.get("/estado")
async def estado_sistema(config: Configuracion = Depends(get_settings)):
return {
"app": config.app_name,
"version": config.app_version,
"entorno": config.environment,
"debug": config.debug,
}
@app.get("/feature-flags")
async def obtener_flags(config: Configuracion = Depends(get_settings)):
# Algunos flags pueden estar en la configuración
return {
"modo_mantenimiento": getattr(config, "modo_mantenimiento", False),
"registro_habilitado": True,
}
Testing con configuración sobreescrita
Para los tests, sobreescribe la dependencia get_settings con una configuración de test:
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.config import Configuracion, get_settings
class ConfiguracionTest(Configuracion):
database_url: str = "sqlite://"
secret_key: str = "test-secret-key-123"
environment: str = "desarrollo"
debug: bool = True
@pytest.fixture
def client():
config_test = ConfiguracionTest()
app.dependency_overrides[get_settings] = lambda: config_test
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
Este patrón garantiza que los tests usan siempre una configuración predecible y no dependen de variables de entorno del sistema del desarrollador.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en FastAPI
Documentación oficial de FastAPI
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, FastAPI es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de FastAPI
Explora más contenido relacionado con FastAPI y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Instalar y configurar pydantic-settings para gestionar la configuración de la aplicación. Definir modelos de configuración con tipos y valores por defecto seguros. Cargar configuración desde variables de entorno y archivos .env. Inyectar la configuración como dependencia en los endpoints de FastAPI. Usar configuraciones específicas por entorno (desarrollo, staging, producción).