Écrire la configuration d’une application Python (clés d’API, identifiants de
base de données, mode debug…) directement dans le code source pose vite
problème. Les variables d’environnement séparent la configuration du code, et
python-dotenv permet de les définir dans un fichier .env pendant le
développement.
On retrouve python-dotenv dans tous les types de projets Python : API Flask
ou FastAPI, script de traitement de données, application Django. Si vous avez
déjà dockerisé une application, vous avez probablement manipulé des variables
d’environnement : avec python-dotenv, vous travaillez de la même manière en
développement local.
Dans cet article :
- Pourquoi utiliser des variables d’environnement ?
- Installation
- Créer un fichier .env
- Charger les variables dans Python
- Priorité des variables
- Spécifier un fichier différent
- Exemple concret : une connexion à une base de données
- Protéger ses secrets avec .gitignore
- Utilisation avec Docker
Pourquoi utiliser des variables d’environnement ?
Imaginons une application qui se connecte à une base de données :
# À ne pas faire : les identifiants en dur dans le code
import psycopg2
conn = psycopg2.connect(
host="localhost",
database="mon_app",
user="admin",
password="motdepasse_secret"
)
Le mot de passe est visible par toute personne qui a accès au dépôt Git. Pour passer du développement à la production, il faut modifier le code, et chaque développeur de l’équipe doit changer le fichier pour y mettre ses propres identifiants.
On stocke plutôt ces valeurs dans des variables d’environnement. C’est l’un des principes de la méthodologie twelve-factor : la configuration doit être séparée du code.
Installation
Installez python-dotenv avec pip, de préférence dans un environnement
virtuel (pensez à l’activer avant) :
pip install python-dotenv
Les exemples de cet article ont été testés avec python-dotenv 1.2.4.
Créer un fichier .env
Le fichier .env se place à la racine de votre projet. Il contient les
variables sous la forme CLÉ=valeur, une par ligne :
# Configuration de la base de données
DB_HOST=localhost
DB_PORT=5432
DB_NAME=mon_app
DB_USER=admin
DB_PASSWORD=motdepasse_secret
# Configuration de l'application
DEBUG=true
SECRET_KEY=ma-cle-secrete-tres-longue
API_KEY=sk-1234567890abcdef
Quelques règles de syntaxe à connaître :
- les lignes vides et celles qui commencent par
#(les commentaires) sont ignorées ; - python-dotenv accepte des espaces autour du
=, mais un shell les refuse : mieux vaut s’en passer ; - les guillemets ne sont pas obligatoires, même si la valeur contient des espaces, mais ils gardent le fichier lisible par un shell (
APP_NAME="Mon Application").
Une variable peut aussi faire référence à d’autres variables, avec la forme
${NOM} :
DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
Seule la forme avec accolades est interprétée : $DB_USER reste tel quel.
Charger les variables dans Python
Utilisation de base
La fonction load_dotenv() lit le fichier .env et ajoute ses variables à
l’environnement du processus. On y accède ensuite avec os.getenv() :
import os
from dotenv import load_dotenv
# Charger le fichier .env
load_dotenv()
# Lire les variables
db_host = os.getenv("DB_HOST")
db_port = int(os.getenv("DB_PORT", "5432"))
debug = os.getenv("DEBUG", "false").lower() == "true"
print(f"Connexion à {db_host}:{db_port}")
print(f"Mode debug : {debug}")
Avec le fichier .env précédent, on obtient :
Connexion à localhost:5432
Mode debug : True
os.getenv() accepte un second paramètre qui sert de valeur par défaut si la
variable n’est pas définie, comme "5432" pour le port. Notez aussi que les
variables d’environnement sont toujours des chaînes de caractères : c’est à
vous de les convertir, d’où le int() et la comparaison avec "true".
Si le fichier .env n’existe pas, load_dotenv() ne lève pas d’erreur et
retourne simplement False. C’est ce comportement silencieux qui permet au même
code de fonctionner en local (où le .env est présent) et en production (où les
variables sont déjà injectées dans l’environnement par l’orchestrateur).
Utilisation avec dotenv_values()
Si vous préférez ne pas modifier les variables d’environnement du processus,
dotenv_values() retourne un dictionnaire :
from dotenv import dotenv_values
config = dotenv_values(".env")
db_host = config["DB_HOST"]
db_port = int(config.get("DB_PORT", "5432"))
Cette approche est pratique quand on veut isoler la configuration sans affecter l’environnement global, par exemple dans des tests.
Priorité des variables
Par défaut, load_dotenv() ne remplace pas les variables d’environnement
déjà définies. Si DB_HOST existe déjà dans l’environnement système, la
valeur du .env est ignorée.
# Les variables système ont la priorité
load_dotenv() # DB_HOST du .env ignoré si déjà défini dans l'environnement
Ce comportement est voulu : en production, on définit les variables
d’environnement directement (via Docker, systemd, le fournisseur cloud…), et le
fichier .env ne sert qu’en développement local.
Pour forcer le remplacement des variables existantes, utilisez le paramètre
override :
# Forcer l'utilisation des valeurs du .env
load_dotenv(override=True)
Attention avec override=True en production : il peut écraser des variables
d’environnement définies volontairement par l’infrastructure.
Spécifier un fichier différent
Par défaut, load_dotenv() cherche un fichier .env dans le dossier du script
Python qui l’appelle, puis dans les dossiers parents. On peut aussi lui donner
un chemin explicitement :
from dotenv import load_dotenv
# Charger un fichier spécifique
load_dotenv(".env.production")
Ce mécanisme sert à gérer plusieurs environnements avec des fichiers distincts :
mon_projet/
.env # Configuration par défaut (dev)
.env.test # Configuration pour les tests
.env.production # Configuration de production
app.py
import os
from dotenv import load_dotenv
env = os.getenv("APP_ENV", "development")
if env == "test":
load_dotenv(".env.test")
elif env == "production":
load_dotenv(".env.production")
else:
load_dotenv() # .env par défaut
Attention, contrairement à la recherche par défaut, un chemin relatif comme
.env.test part du répertoire courant, pas du dossier du script : il faut
lancer le programme depuis la racine du projet, ou construire un chemin
absolu avec Path(__file__).parent / ".env.test".
La recherche par défaut est faite par la fonction find_dotenv(), que
load_dotenv() appelle quand on ne lui passe pas de chemin. On peut l’appeler
soi-même pour changer son comportement, par exemple pour chercher le .env à
partir du répertoire courant plutôt qu’à partir du script :
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv(usecwd=True))
Exemple concret : une connexion à une base de données
Voici un exemple complet qui montre comment structurer la configuration d’une connexion à PostgreSQL :
# .env
DB_HOST=localhost
DB_PORT=5432
DB_NAME=mon_app
DB_USER=admin
DB_PASSWORD=motdepasse_secret
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = int(os.getenv("DB_PORT", "5432"))
DB_NAME = os.getenv("DB_NAME")
DB_USER = os.getenv("DB_USER")
DB_PASSWORD = os.getenv("DB_PASSWORD")
@property
def database_url(self):
return (
f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}"
f"@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
)
config = Config()
# app.py
from config import config
print(f"Connexion à : {config.database_url}")
Ce qui affiche :
Connexion à : postgresql://admin:motdepasse_secret@localhost:5432/mon_app
Une classe Config de ce genre est très courante dans les projets Flask et
FastAPI : toute la configuration est regroupée au même endroit. Pour une
variable obligatoire, comme le mot de passe, on peut utiliser
os.environ["DB_PASSWORD"] à la place de os.getenv() : il lève une
KeyError si la variable n’existe pas, et l’application s’arrête dès son
démarrage au lieu d’échouer plus tard.
Pour aller plus loin, avec du typage strict et une validation automatique au
démarrage, on peut utiliser pydantic-settings,
souvent associé à FastAPI. Pydantic se charge alors
de convertir chaque valeur dans le type déclaré (int, bool…) et de lever
une erreur explicite si une variable obligatoire est absente.
Protéger ses secrets avec .gitignore
Le fichier .env contient des secrets. Il ne doit jamais être commité dans le
dépôt Git. Ajoutez-le à votre .gitignore :
# Variables d'environnement
.env
.env.*
!.env.example
En revanche, on peut créer un fichier .env.example (sans les vraies
valeurs) pour documenter les variables nécessaires :
# .env.example - Copiez ce fichier en .env et remplissez les valeurs
DB_HOST=localhost
DB_PORT=5432
DB_NAME=
DB_USER=
DB_PASSWORD=
DEBUG=true
SECRET_KEY=
API_KEY=
Ce fichier .env.example peut être commité (c’est le rôle de la ligne
!.env.example du .gitignore) : il sert de documentation pour les autres
développeurs qui rejoignent le projet. N’y mettez jamais de valeurs de
production.
Utilisation avec Docker
Si vous dockerisez votre application, le même code fonctionne en local et dans
le conteneur. En développement, le fichier .env est chargé par
python-dotenv. En production, les variables sont injectées par Docker :
# docker-compose.yml
services:
app:
build: .
env_file:
- .env
Ou directement via des variables d’environnement :
services:
app:
build: .
environment:
- DB_HOST=db
- DB_PORT=5432
- DB_NAME=mon_app
Dans les deux cas, le code Python reste identique : dans le conteneur, les
variables viennent de Docker, et load_dotenv() ne trouve rien à charger (il
retourne False silencieusement). Cela suppose que le .env ne soit pas copié
dans l’image : avec un COPY . . dans le Dockerfile, ajoutez-le à votre
.dockerignore. En local comme dans le conteneur, os.getenv("DB_HOST")
récupère la bonne valeur :
load_dotenv() # En dev : charge le .env / En Docker : no-op, les variables sont déjà là
db_host = os.getenv("DB_HOST")