Aller au contenu

Bonnes pratiques Python

Python encourage un style de code lisible et expressif, formalise par une serie de conventions regroupees dans la PEP 8. Au-delà du formatage, Python offre des idiomes puissants (comprehensions, generateurs, context managers) qui permettent d'écrire un code plus concis et plus efficace. Cette section recense les pratiques recommandees et les pieges les plus fréquents.


PEP 8 — Conventions de style

PEP 8 est le guide de style officiel de Python. Les points essentiels :

Nommage

Élément Convention Exemple
Variable snake_case nombre_articles
Fonction snake_case calculer_total()
Classe PascalCase CommandeUtilisateur
Constante UPPER_SNAKE_CASE TAUX_TVA = 0.20
Module snake_case traitement_donnees.py
Package lowercase monpaquet/
Attribut prive _prefixe self._cache
Attribut "interne" __double self.__secret (name mangling)

Mise en forme

# Longueur de ligne : 88 caracteres (standard ruff/black)
# PEP 8 recommande 79, mais 88-100 est la norme moderne

# Imports : stdlib, tiers, local — separes par une ligne vide
import os
import sys
from pathlib import Path

import fastapi
from sqlalchemy.orm import Session

from monpaquet.models import Item

# Espaces autour des operateurs
x = 10 + 2 * (5 - 1)

# Pas d'espace avant les parentheses d'appel
result = ma_fonction(arg1, arg2)

# Virgule finale dans les structures multi-lignes
config = {
    "host": "localhost",
    "port": 8000,
    "debug": False,  # virgule finale = diff plus propre en git
}

Idiomes Python

Comprehensions

# Liste de carres des nombres pairs
carres_pairs = [x**2 for x in range(20) if x % 2 == 0]

# Dictionnaire inverse (valeur -> cle)
original = {"a": 1, "b": 2, "c": 3}
inverse = {v: k for k, v in original.items()}

# Set de mots uniques en minuscules
texte = "Python est Simple Python est Lisible"
mots_uniques = {mot.lower() for mot in texte.split()}

# Expression generatrice (evaluee paresseusement)
total = sum(x**2 for x in range(1000))  # Ne cree pas de liste intermediaire

Context managers

# Ouverture de fichier — fermeture garantie
with open("donnees.csv", encoding="utf-8") as f:
    contenu = f.read()

# Plusieurs ressources simultanees
with open("source.txt") as src, open("dest.txt", "w") as dst:
    dst.write(src.read())

# Context manager personnalise avec contextlib
from contextlib import contextmanager
import time


@contextmanager
def chronometre(nom: str):
    debut = time.perf_counter()
    try:
        yield
    finally:
        duree = time.perf_counter() - debut
        print(f"{nom}: {duree:.3f}s")


with chronometre("traitement"):
    result = sum(range(1_000_000))

Unpacking et spread

# Unpacking de sequences
premier, *reste = [1, 2, 3, 4, 5]
# premier = 1, reste = [2, 3, 4, 5]

*debut, dernier = [1, 2, 3, 4, 5]
# debut = [1, 2, 3, 4], dernier = 5

# Echange de variables sans temporaire
a, b = 1, 2
a, b = b, a  # a=2, b=1

# Merge de dictionnaires (Python 3.9+)
defaults = {"timeout": 30, "retries": 3}
overrides = {"timeout": 60}
config = {**defaults, **overrides}   # Python 3.5+
config = defaults | overrides        # Python 3.9+

Generateurs

def lire_lignes_csv_par_batch(chemin: str, taille: int = 1000):
    """
    Lit un CSV volumineux par lots sans tout charger en memoire.
    Utilise un generateur pour une empreinte memoire constante.
    """
    import csv

    batch: list[dict] = []
    with open(chemin, encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for ligne in reader:
            batch.append(ligne)
            if len(batch) >= taille:
                yield batch
                batch = []
    if batch:  # Dernier lot (possiblement incomplet)
        yield batch


# Usage
for lot in lire_lignes_csv_par_batch("ventes.csv", taille=500):
    traiter_lot(lot)

Anti-patterns courants

Argument mutable par défaut

# MAUVAIS : la liste est partagee entre tous les appels
def ajouter_item(item: str, liste: list = []) -> list:
    liste.append(item)
    return liste

ajouter_item("a")  # ["a"]
ajouter_item("b")  # ["a", "b"] — surprenant !

# CORRECT : utiliser None comme sentinel
def ajouter_item(item: str, liste: list | None = None) -> list:
    if liste is None:
        liste = []
    liste.append(item)
    return liste

Bare except

# MAUVAIS : capture toutes les exceptions, y compris KeyboardInterrupt
try:
    faire_quelque_chose()
except:
    pass

# CORRECT : capter uniquement les exceptions prevues
try:
    faire_quelque_chose()
except ValueError as e:
    logger.warning("Valeur invalide : %s", e)
except (ConnectionError, TimeoutError) as e:
    logger.error("Erreur reseau : %s", e)
    raise

État global mutable

# MAUVAIS : etat global difficile a tester et a raisonner
_cache = {}

def obtenir_config(cle: str) -> str:
    return _cache.get(cle, "")

# CORRECT : encapsuler dans une classe ou utiliser l'injection de dependances
from functools import lru_cache


class ConfigService:
    def __init__(self) -> None:
        self._cache: dict[str, str] = {}

    def obtenir(self, cle: str) -> str:
        return self._cache.get(cle, "")

    def definir(self, cle: str, valeur: str) -> None:
        self._cache[cle] = valeur

Comparaisons incorrectes

# MAUVAIS
if x == None:
    pass
if x == True:
    pass
if type(x) == list:
    pass

# CORRECT
if x is None:
    pass
if x is True:
    pass
if isinstance(x, list):
    pass

Gestion d'erreurs — EAFP vs LBYL

Python favorise le style EAFP (Easier to Ask Forgiveness than Permission) plutôt que LBYL (Look Before You Leap).

# LBYL (style C/Java) — verifier avant d'agir
if "cle" in dictionnaire and isinstance(dictionnaire["cle"], int):
    valeur = dictionnaire["cle"]
else:
    valeur = 0

# EAFP (style pythonique) — essayer et gerer l'echec
try:
    valeur = dictionnaire["cle"]
except (KeyError, TypeError):
    valeur = 0

# Encore plus pythonique avec .get()
valeur = dictionnaire.get("cle", 0)

Exceptions personnalisees

class ErreurApplication(Exception):
    """Classe de base pour les erreurs de l'application."""
    pass


class ErreurValidation(ErreurApplication):
    """Donnee invalide soumise a l'application."""

    def __init__(self, champ: str, message: str) -> None:
        self.champ = champ
        self.message = message
        super().__init__(f"Champ '{champ}': {message}")


class ErreurRessourceIntrouvable(ErreurApplication):
    """Ressource demandee inexistante."""

    def __init__(self, type_ressource: str, identifiant: int | str) -> None:
        self.type_ressource = type_ressource
        self.identifiant = identifiant
        super().__init__(f"{type_ressource} {identifiant!r} introuvable")


# Usage
def obtenir_utilisateur(user_id: int) -> dict:
    resultat = db.find(user_id)
    if resultat is None:
        raise ErreurRessourceIntrouvable("Utilisateur", user_id)
    return resultat

Performance — slots, caching, generateurs

from functools import lru_cache, cache
from dataclasses import dataclass


# __slots__ : reduit la memoire des instances (pas de __dict__)
class PointOptimise:
    __slots__ = ("x", "y")

    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y


# @dataclass avec slots (Python 3.10+)
@dataclass(slots=True, frozen=True)
class Coordonnee:
    latitude: float
    longitude: float


# Memoisation des appels couteux
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


# @cache = @lru_cache(maxsize=None) — sans limite
@cache
def calculer_factorielle(n: int) -> int:
    if n == 0:
        return 1
    return n * calculer_factorielle(n - 1)

Profiler avant d'optimiser

Ne jamais optimiser sans mesure. Utilisez cProfile ou py-spy pour identifier les vrais goulots d'étranglement avant de modifier le code. Voir le chapitre Écosystème pour les outils de profiling.