Structurer un projet Python data : arborescence, découpage et erreurs qui coûtent cher
Une structure de projet Python data tient en 5 dossiers : src/ pour le code, tests/ pour les tests, data/ pour les données locales non versionnées, notebooks/ pour l'exploration, et la racine pour la configuration. Tout le reste est de la variation.
Ce qui distingue un projet lisible d'un projet qui décourage, ce n'est pas le nombre de dossiers : c'est la règle de découpage à l'intérieur de src/.
L'arborescence minimale
projet-ventes/
├── pyproject.toml # dependances + config des outils
├── uv.lock # versions figees
├── README.md # quoi, pourquoi, comment lancer
├── .gitignore
├── src/
│ └── ventes/
│ ├── __init__.py
│ ├── config.py # constantes, chemins
│ ├── extract.py # lecture des sources
│ ├── transform.py # nettoyage, calculs metier
│ ├── load.py # ecriture des sorties
│ └── cli.py # point d'entree
├── tests/
│ ├── test_transform.py
│ └── conftest.py # fixtures partagees
├── notebooks/
│ └── 01-exploration.ipynb
└── data/
├── raw/ # jamais modifie
├── interim/ # etapes intermediaires
└── processed/ # sorties finales
Trois décisions structurantes s'y cachent.
1. src/ contient un package nommé, pas des fichiers en vrac
src/ventes/extract.py plutôt que extract.py à la racine. Raison pratique : vous importez from ventes.transform import calculer_ca depuis n'importe où, y compris depuis tests/, sans bricoler sys.path. C'est la seule mise en place qui évite les imports cassés en fonction du répertoire courant.
2. data/ est découpé en raw / interim / processed
raw/ est en lecture seule, toujours. Le jour où un traitement écrase un fichier source, vous perdez la seule chose non reproductible du projet.
Cette convention vient de cookiecutter-data-science et elle a une vertu de communication : n'importe qui voit où se trouve la donnée de départ et où se trouve le livrable.
3. notebooks/ existe et est numéroté
Ne bannissez pas les notebooks, rangez-les. Un préfixe numérique (01-, 02-) suffit à raconter l'ordre de l'exploration. Le passage du notebook au module est traité en détail dans le guide passer du notebook au script Python.
La règle de découpage : par étape du flux, pas par type d'objet
C'est là que la plupart des projets data dérapent.
| Découpage | Exemple | Verdict |
|---|---|---|
| Par étape du flux | extract.py, transform.py, load.py |
✅ chaque fichier a une frontière claire |
| Par objet métier | clients.py, produits.py, ventes.py |
✅ valable si le métier domine |
| Par type technique | fonctions.py, utils.py, helpers.py |
❌ devient un dépotoir en 3 semaines |
| Un seul fichier | script.py de 900 lignes |
❌ intestable, irrelisable |
utils.py est le symptôme le plus fiable d'un projet qui part à la dérive : c'est le fichier où l'on met ce qu'on n'a pas su nommer. Si vous en avez un et qu'il dépasse 100 lignes, il contient au moins deux modules qui s'ignorent.
Le .gitignore data, qui n'est pas le .gitignore Python
Le template Python standard ne connaît rien à la data. Ajoutez :
# Donnees : jamais dans Git
data/raw/*
data/interim/*
data/processed/*
!data/**/.gitkeep
# Sorties de notebooks
.ipynb_checkpoints/
# Secrets
.env
*.pem
credentials.json
# Artefacts
*.parquet
*.duckdb
Deux erreurs coûteuses évitées ici :
- Un CSV de 200 Mo committé alourdit le dépôt pour toujours, même supprimé ensuite, parce que l'objet reste dans l'historique Git.
- Un
.envavec la chaîne de connexion Postgres committé une fois est un secret compromis, même après suppression du fichier. La procédure correcte est la rotation de la clé, pas legit rm.
Les .gitkeep conservent l'arborescence vide dans Git : c'est ce qui permet au collègue qui clone d'avoir les bons dossiers sans les données.
Le fichier de configuration, un seul, à un seul endroit
# src/ventes/config.py
from pathlib import Path
import os
RACINE = Path(__file__).resolve().parents[2]
DATA_RAW = RACINE / "data" / "raw"
DATA_PROCESSED = RACINE / "data" / "processed"
# Secrets : depuis l'environnement, jamais en dur
DB_URL = os.environ["DB_URL"]
# Regles metier
SEUIL_OUTLIER = 3.0
COLONNES_REQUISES = ["date", "region", "produit", "quantite", "prix_unitaire"]
parents[2] remonte de src/ventes/config.py à la racine du projet. C'est ce qui rend les chemins indépendants du répertoire depuis lequel on lance le script.
os.environ["DB_URL"] avec des crochets, pas os.environ.get("DB_URL") : vous voulez que le script échoue immédiatement et bruyamment si la variable manque, pas qu'il parte avec None et casse trois étapes plus loin avec un message incompréhensible.
Le README, en 5 sections et pas plus
Un README de projet data répond à cinq questions, dans cet ordre :
- Quel problème ce projet résout (2 lignes)
- Quelles données il consomme (source, fréquence, volumétrie)
- Comment le lancer (les 3 commandes exactes, copiables)
- Ce qu'il produit (fichier de sortie, format, où)
- Les pièges connus (la limite de l'API, le champ mal typé à la source)
La section 5 est celle qu'on n'écrit jamais et qui fait gagner le plus de temps au suivant. Si vous manquez de temps, le générateur de README produit ce squelette à partir de quelques champs.
Les 5 erreurs qui rendent un projet illisible
| Erreur | Conséquence | Correction |
|---|---|---|
| Chemins absolus dans le code | Injouable ailleurs | Path(__file__) |
utils.py fourre-tout |
Personne ne sait où chercher | Renommer par responsabilité |
| Données committées | Dépôt de 800 Mo | .gitignore + rotation si secret |
Aucun __init__.py |
Imports qui dépendent du cwd | Package nommé sous src/ |
| Config dupliquée dans 4 fichiers | Un changement, 4 oublis | Un seul config.py |
« Mon équipe s'en fout de la structure »
Tant que le projet est à une personne, c'est vrai. La structure ne se rentabilise pas le jour où vous l'écrivez, elle se rentabilise le jour où quelqu'un doit reprendre le code, y compris vous dans six mois.
Le signal concret : mesurez le temps entre « on me demande une modif » et « la modif est livrée ». Sur un projet en un seul fichier, ce délai grossit avec la taille du fichier. Sur un projet découpé, il reste stable. C'est le seul argument qui porte en réunion.
Avant de reprendre un projet existant, deux réflexes utiles : vérifier que l'historique Git est propre (le cours interactif gratuit Git pour data analyst fait travailler exactement ces situations d'équipe, et les 8 situations Git qui coincent le plus donnent la commande exacte pour chacune), puis mesurer l'état actuel avec le quiz code production-ready.
La méthode complète, appliquée de bout en bout sur un fil rouge d'entreprise unique, du dépôt vide jusqu'à la CI, c'est l'objet de la formation DataCraft, pour structurer un projet data de bout en bout.
Questions fréquentes
src/ ou pas src/ ?
Avec src/. Sans lui, un import ventes peut résoudre vers le dossier local au lieu du package installé, et vos tests passent pour de mauvaises raisons.
Faut-il un dossier scripts/ ?
Seulement pour les tâches ponctuelles jetables (migration, rattrapage). Tout ce qui est récurrent appartient à src/ avec un point d'entrée.
Où mettre les requêtes SQL ?
Dans des fichiers .sql sous src/ventes/sql/, chargés par le code. Le SQL en triple guillemets dans du Python n'est ni colorisé, ni diffable proprement, ni réutilisable.

Approfondir avec mon livre
"Business Intelligence avec Python" - Le guide complet pour maîtriser l'analyse de données
Voir sur Amazon →