datacraftPythonstructure projetbonnes pratiquesproductiondata analyst

Structurer un projet Python data : arborescence, découpage et erreurs qui coûtent cher

GP
Gaël Penessot

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 .env avec 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 le git 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 :

  1. Quel problème ce projet résout (2 lignes)
  2. Quelles données il consomme (source, fréquence, volumétrie)
  3. Comment le lancer (les 3 commandes exactes, copiables)
  4. Ce qu'il produit (fichier de sortie, format, où)
  5. 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.

Livre Business Intelligence avec Python

Approfondir avec mon livre

"Business Intelligence avec Python" - Le guide complet pour maîtriser l'analyse de données

Voir sur Amazon →

Formation recommandée

DataCraft

Du notebook chaotique au repo production-ready : structure, tests, Git, CI. Le software engineering appliqué à la data, sur un fil rouge d'entreprise unique.

Voir la formation →

Ne manque rien de l'actualité data

Rejoins +1000 professionnels qui reçoivent chaque semaine mes analyses, conseils et découvertes data.

S'abonner gratuitement
Prochaine révision : Trimestre prochain