Gestion de la Configuration en Python
Externalisez la configuration du code en utilisant des variables d'environnement et des paramètres typés. Une configuration bien gérée permet à un même code de s'exécuter dans n'importe quel environnement sans modification.
Quand utiliser cette compétence
- Mettre en place le système de configuration d'un nouveau projet
- Migrer des valeurs codées en dur vers des variables d'environnement
- Implémenter pydantic-settings pour une configuration typée
- Gérer les secrets et valeurs sensibles
- Créer des paramètres spécifiques à chaque environnement (dev/staging/prod)
- Valider la configuration au démarrage de l'application
Concepts fondamentaux
1. Configuration externalisée
Toutes les valeurs spécifiques à un environnement (URLs, secrets, feature flags) proviennent de variables d'environnement, pas du code.
2. Paramètres typés
Analysez et validez la configuration en objets typés au démarrage, et non dispersés dans le code.
3. Échouer rapidement
Validez toute la configuration requise au démarrage de l'application. Une configuration manquante doit s'arrêter immédiatement avec un message clair.
4. Valeurs par défaut raisonnables
Fournissez des valeurs par défaut raisonnables pour le développement local tout en exigeant des valeurs explicites pour les paramètres sensibles.
Démarrage rapide
from pydantic_settings import BaseSettings
from pydantic import Field
class Settings(BaseSettings):
database_url: str = Field(alias="DATABASE_URL")
api_key: str = Field(alias="API_KEY")
debug: bool = Field(default=False, alias="DEBUG")
settings = Settings() # Charge depuis l'environnement
Modèles fondamentaux
Modèle 1 : Paramètres typés avec Pydantic
Créez une classe de paramètres centralisée qui charge et valide toute la configuration.
from pydantic_settings import BaseSettings
from pydantic import Field, PostgresDsn, ValidationError
import sys
class Settings(BaseSettings):
"""Configuration de l'application chargée depuis les variables d'environnement."""
# Base de données
db_host: str = Field(alias="DB_HOST")
db_port: int = Field(default=5432, alias="DB_PORT")
db_name: str = Field(alias="DB_NAME")
db_user: str = Field(alias="DB_USER")
db_password: str = Field(alias="DB_PASSWORD")
# Redis
redis_url: str = Field(default="redis://localhost:6379", alias="REDIS_URL")
# Clés API
api_secret_key: str = Field(alias="API_SECRET_KEY")
# Feature flags
enable_new_feature: bool = Field(default=False, alias="ENABLE_NEW_FEATURE")
model_config = {
"env_file": ".env",
"env_file_encoding": "utf-8",
}
# Créez une instance singleton au chargement du module
try:
settings = Settings()
except ValidationError as e:
print(f"Erreur de configuration :\n{e}")
sys.exit(1)
Importez settings dans toute votre application :
from myapp.config import settings
def get_database_connection():
return connect(
host=settings.db_host,
port=settings.db_port,
database=settings.db_name,
)
Modèle 2 : Échouer rapidement sur configuration manquante
Les paramètres requis doivent arrêter l'application immédiatement avec une erreur claire.
from pydantic_settings import BaseSettings
from pydantic import Field, ValidationError
import sys
class Settings(BaseSettings):
# Requis - pas de défaut signifie que ce paramètre doit être défini
api_key: str = Field(alias="API_KEY")
database_url: str = Field(alias="DATABASE_URL")
# Optionnel avec défauts
log_level: str = Field(default="INFO", alias="LOG_LEVEL")
try:
settings = Settings()
except ValidationError as e:
print("=" * 60)
print("ERREUR DE CONFIGURATION")
print("=" * 60)
for error in e.errors():
field = error["loc"][0]
print(f" - {field} : {error['msg']}")
print("\nVeuillez définir les variables d'environnement requises.")
sys.exit(1)
Une erreur claire au démarrage est préférable à un échec cryptique None au milieu d'une requête.
Modèle 3 : Valeurs par défaut pour le développement local
Fournissez des valeurs par défaut raisonnables pour le développement local tout en exigeant des valeurs explicites pour les secrets.
class Settings(BaseSettings):
# A une valeur par défaut locale, mais prod l'écrasera
db_host: str = Field(default="localhost", alias="DB_HOST")
db_port: int = Field(default=5432, alias="DB_PORT")
# Toujours requis - pas de défaut pour les secrets
db_password: str = Field(alias="DB_PASSWORD")
api_secret_key: str = Field(alias="API_SECRET_KEY")
# Commodité de développement
debug: bool = Field(default=False, alias="DEBUG")
model_config = {"env_file": ".env"}
Créez un fichier .env pour le développement local (ne le committez jamais) :
# .env (ajoutez à .gitignore)
DB_PASSWORD=local_dev_password
API_SECRET_KEY=dev-secret-key
DEBUG=true
Modèle 4 : Variables d'environnement avec espaces de noms
Préfixez les variables associées pour plus de clarté et un débogage facile.
# Configuration de la base de données
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=admin
DB_PASSWORD=secret
# Configuration Redis
REDIS_URL=redis://localhost:6379
REDIS_MAX_CONNECTIONS=10
# Authentification
AUTH_SECRET_KEY=your-secret-key
AUTH_TOKEN_EXPIRY_SECONDS=3600
AUTH_ALGORITHM=HS256
# Feature flags
FEATURE_NEW_CHECKOUT=true
FEATURE_BETA_UI=false
Rend env | grep DB_ utile pour le débogage.
Modèles avancés
Modèle 5 : Conversion de type
Pydantic gère automatiquement les conversions courantes.
from pydantic_settings import BaseSettings
from pydantic import Field, field_validator
class Settings(BaseSettings):
# Convertit automatiquement "true", "1", "yes" en True
debug: bool = False
# Convertit automatiquement string en int
max_connections: int = 100
# Analyse une string séparée par des virgules en liste
allowed_hosts: list[str] = Field(default_factory=list)
@field_validator("allowed_hosts", mode="before")
@classmethod
def parse_allowed_hosts(cls, v: str | list[str]) -> list[str]:
if isinstance(v, str):
return [host.strip() for host in v.split(",") if host.strip()]
return v
Utilisation :
ALLOWED_HOSTS=example.com,api.example.com,localhost
MAX_CONNECTIONS=50
DEBUG=true
Modèle 6 : Configuration spécifique à l'environnement
Utilisez une énumération d'environnement pour modifier le comportement.
from enum import Enum
from pydantic_settings import BaseSettings
from pydantic import Field, computed_field
class Environment(str, Enum):
LOCAL = "local"
STAGING = "staging"
PRODUCTION = "production"
class Settings(BaseSettings):
environment: Environment = Field(
default=Environment.LOCAL,
alias="ENVIRONMENT",
)
# Paramètres qui varient selon l'environnement
log_level: str = Field(default="DEBUG", alias="LOG_LEVEL")
@computed_field
@property
def is_production(self) -> bool:
return self.environment == Environment.PRODUCTION
@computed_field
@property
def is_local(self) -> bool:
return self.environment == Environment.LOCAL
# Utilisation
if settings.is_production:
configure_production_logging()
else:
configure_debug_logging()
Modèle 7 : Groupes de configuration imbriqués
Organisez les paramètres associés en modèles imbriqués.
from pydantic import BaseModel
from pydantic_settings import BaseSettings
class DatabaseSettings(BaseModel):
host: str = "localhost"
port: int = 5432
name: str
user: str
password: str
class RedisSettings(BaseModel):
url: str = "redis://localhost:6379"
max_connections: int = 10
class Settings(BaseSettings):
database: DatabaseSettings
redis: RedisSettings
debug: bool = False
model_config = {
"env_nested_delimiter": "__",
"env_file": ".env",
}
Les variables d'environnement utilisent un double tiret bas pour l'imbrication :
DATABASE__HOST=db.example.com
DATABASE__PORT=5432
DATABASE__NAME=myapp
DATABASE__USER=admin
DATABASE__PASSWORD=secret
REDIS__URL=redis://redis.example.com:6379
Modèle 8 : Secrets à partir de fichiers
Pour les environnements de conteneurs, lisez les secrets depuis des fichiers montés.
from pydantic_settings import BaseSettings
from pydantic import Field
from pathlib import Path
class Settings(BaseSettings):
# Lire depuis une variable d'environnement ou un fichier
db_password: str = Field(alias="DB_PASSWORD")
model_config = {
"secrets_dir": "/run/secrets", # Emplacement des secrets Docker
}
Pydantic cherchera /run/secrets/db_password si la variable d'environnement n'est pas définie.
Modèle 9 : Validation de la configuration
Ajoutez une validation personnalisée pour les exigences complexes.
from pydantic_settings import BaseSettings
from pydantic import Field, model_validator
class Settings(BaseSettings):
db_host: str = Field(alias="DB_HOST")
db_port: int = Field(alias="DB_PORT")
read_replica_host: str | None = Field(default=None, alias="READ_REPLICA_HOST")
read_replica_port: int = Field(default=5432, alias="READ_REPLICA_PORT")
@model_validator(mode="after")
def validate_replica_settings(self):
if self.read_replica_host and self.read_replica_port == self.db_port:
if self.read_replica_host == self.db_host:
raise ValueError(
"Le replica en lecture ne peut pas être la même que la base de données primaire"
)
return self
Résumé des bonnes pratiques
- Ne codez jamais la configuration en dur - Toutes les valeurs spécifiques à un environnement proviennent de variables d'environnement
- Utilisez les paramètres typés - pydantic-settings avec validation
- Échouez rapidement - Arrêtez sur configuration requise manquante au démarrage
- Fournissez des valeurs par défaut pour le dev - Facilitez le développement local
- Ne committez jamais les secrets - Utilisez des fichiers
.env(gitignorés) ou des gestionnaires de secrets - Utilisez des espaces de noms pour les variables -
DB_HOST,REDIS_URLpour plus de clarté - Importez le singleton de paramètres - N'appelez pas
os.getenv()dans tout le code - Documentez toutes les variables - Le README doit lister les variables d'environnement requises
- Validez tôt - Vérifiez la corrección de la configuration au démarrage
- Utilisez secrets_dir - Supportez les secrets montés dans les conteneurs