---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Bonnes pratiques et idiomes pythoniques

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

## Le Zen de Python

Avant d'aborder les règles pratiques, il convient de s'arrêter sur la philosophie qui sous-tend toute l'esthétique du langage. Le **Zen de Python** est un ensemble de dix-neuf aphorismes rédigés par Tim Peters et intégrés à Python comme un œuf de Pâques accessible via `import this`. Ce texte n'est pas qu'une curiosité : il constitue une véritable charte de conception que les Pythonistes expérimentés intériorisent et appliquent naturellement.

```{code-cell} python
import this
```

Quelques aphorismes méritent une attention particulière dans le contexte des bonnes pratiques. *Readability counts* (la lisibilité compte) : en Python, le code est lu beaucoup plus souvent qu'il n'est écrit — par des collègues, par vous-même six mois plus tard, par des outils d'analyse. Toute décision de style qui améliore la lisibilité est préférable. *Explicit is better than implicit* : Python décourage la magie invisible et les comportements implicites. Une variable, une fonction, un module doivent avoir un nom qui dit ce qu'ils font. *Simple is better than complex, Complex is better than complicated* : le but n'est pas la simplicité naïve, mais l'élégance — la solution la plus simple qui fasse correctement le travail. *There should be one obvious way to do it* : contrairement à des langages qui encouragent plusieurs styles pour la même chose, Python converge vers une façon idiomatique pour chaque problème courant. Connaître ces idiomes, c'est parler couramment le langage.

## Nommage PEP 8

La **PEP 8** (*Style Guide for Python Code*) est le document de référence sur le style en Python. Elle couvre l'indentation, la longueur des lignes, les espaces, les imports et, de façon centrale, les conventions de nommage. Ces conventions ne sont pas des règles arbitraires : elles codifient les pratiques unanimement adoptées par l'écosystème, ce qui rend tout code Python reconnaissable et lisible par n'importe quel Pythoniste.

```{prf:definition} Conventions de nommage PEP 8
:label: definition-21-01
Les conventions de nommage Python sont les suivantes :
- **`snake_case`** — variables, fonctions, méthodes, paramètres, modules : `ma_variable`, `calculer_surface`, `nom_fichier`.
- **`SCREAMING_SNAKE_CASE`** — constantes de module : `MAX_CONNEXIONS`, `PI`, `CHEMIN_CONFIG`.
- **`PascalCase`** (ou *CapWords*) — classes, exceptions : `MaClasse`, `ErreurReseau`, `ConfigurationServeur`.
- **`_préfixe_simple`** — convention pour les membres "privés" (pas d'accès externe attendu) : `_cache`, `_calculer_hash`.
- **`__préfixe_double`** — déclenche le *name mangling* dans les classes (protection contre les sous-classes) : `__valeur_interne`.
- **`__dunder__`** — méthodes spéciales réservées au runtime Python : `__init__`, `__len__`, `__iter__`.
```

```{code-cell} python
# Bon nommage PEP 8
TAUX_TVA = 0.20                   # Constante de module

def calculer_prix_ttc(prix_ht: float, taux: float = TAUX_TVA) -> float:
    """Calcule le prix TTC à partir du prix HT et du taux de TVA."""
    return prix_ht * (1 + taux)


class CompteBancaire:
    """Représente un compte bancaire avec solde et historique."""

    def __init__(self, titulaire: str, solde_initial: float = 0.0) -> None:
        self.titulaire = titulaire
        self._solde = solde_initial         # Convention privé
        self._historique: list[float] = []

    @property
    def solde(self) -> float:
        return self._solde

    def déposer(self, montant: float) -> None:
        if montant <= 0:
            raise ValueError("Le montant doit être positif")
        self._solde += montant
        self._historique.append(montant)

    def __repr__(self) -> str:
        return f"CompteBancaire({self.titulaire!r}, solde={self._solde:.2f})"


compte = CompteBancaire("Alice", 1000.0)
compte.déposer(500.0)
print(compte)
print(f"Prix TTC : {calculer_prix_ttc(100):.2f} €")
```

## Compréhensions

Les **compréhensions** sont l'une des caractéristiques les plus emblématiques du style pythonique. Elles permettent de construire des listes, des dictionnaires et des ensembles de façon concise et expressive, en combinant transformation et filtrage en une seule expression.

```{code-cell} python
# ─── Compréhensions de listes ───
carrés = [x**2 for x in range(10)]
pairs = [x for x in range(20) if x % 2 == 0]
mots_longs = [mot.upper() for mot in ["python", "est", "élégant", "et", "expressif"]
              if len(mot) > 3]

print(carrés)
print(pairs)
print(mots_longs)

# Compréhension imbriquée : produit cartésien
combinaisons = [(x, y) for x in range(3) for y in range(3) if x != y]
print(combinaisons)
```

```{code-cell} python
# ─── Compréhensions de dictionnaires ───
noms = ["alice", "bob", "charlie"]
longueurs = {nom: len(nom) for nom in noms}
print(longueurs)

# Inversion d'un dictionnaire (quand les valeurs sont uniques)
original = {"a": 1, "b": 2, "c": 3}
inversé = {v: k for k, v in original.items()}
print(inversé)

# Filtrage d'un dictionnaire
notes = {"alice": 17, "bob": 12, "charlie": 15, "david": 9}
admis = {nom: note for nom, note in notes.items() if note >= 10}
print(admis)
```

```{code-cell} python
# ─── Compréhensions d'ensembles ───
données = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
valeurs_uniques = {x for x in données}
print(valeurs_uniques)

# Lettres uniques dans une phrase, en minuscules
phrase = "Le Zen de Python est une philosophie"
lettres = {c.lower() for c in phrase if c.isalpha()}
print(sorted(lettres))
```

```{prf:remark}
:label: remark-21-01
La règle pratique pour les compréhensions : elles sont appropriées lorsqu'elles tiennent sur une ou deux lignes et restent lisibles. Si la logique est trop complexe — plus d'une condition, plusieurs niveaux d'imbrication, des appels de fonctions multiples — il vaut mieux utiliser une boucle `for` classique ou extraire la logique dans une fonction séparée. La lisibilité prime toujours sur la concision.
```

## Unpacking

L'**unpacking** (*déstructuration*) est la capacité à distribuer les éléments d'un itérable dans plusieurs variables en une seule instruction. C'est un idiome pythonique puissant qui élimine beaucoup d'indexations explicites et rend le code plus auto-documenté.

```{code-cell} python
# Unpacking de tuples et de listes
premier, deuxième, troisième = [10, 20, 30]
print(premier, deuxième, troisième)

# Unpacking avec * (collecte le reste)
tête, *milieu, queue = [1, 2, 3, 4, 5]
print(f"tête={tête}, milieu={milieu}, queue={queue}")

premier, *reste = range(5)
print(f"premier={premier}, reste={reste}")

# Échange de variables — sans variable temporaire
a, b = 10, 20
a, b = b, a
print(f"a={a}, b={b}")

# Unpacking imbriqué
point = (3, (4, 5))
x, (y, z) = point
print(f"x={x}, y={y}, z={z}")
```

```{code-cell} python
# Unpacking dans les boucles
coordonnées = [(1, 2), (3, 4), (5, 6)]
for x, y in coordonnées:
    print(f"  ({x}, {y})")

# enumerate() — idiome fondamental pour itérer avec un indice
fruits = ["pomme", "poire", "cerise"]
for indice, fruit in enumerate(fruits, start=1):
    print(f"  {indice}. {fruit}")

# zip() — itération simultanée sur plusieurs séquences
prénoms = ["Alice", "Bob", "Charlie"]
âges = [30, 25, 35]
for prénom, âge in zip(prénoms, âges):
    print(f"  {prénom} a {âge} ans")
```

```{code-cell} python
# Unpacking dans les appels de fonctions
def créer_connexion(hôte: str, port: int, timeout: float = 30.0) -> str:
    return f"Connexion à {hôte}:{port} (timeout={timeout}s)"

paramètres = ("localhost", 5432)
options = {"timeout": 10.0}

print(créer_connexion(*paramètres, **options))
```

## f-strings avancées

Les **f-strings** (*formatted string literals*), introduites en Python 3.6 et enrichies dans les versions ultérieures, sont le moyen idiomatique de formater des chaînes en Python. Elles sont plus lisibles, plus rapides, et offrent plus de fonctionnalités que `.format()` ou `%`.

```{code-cell} python
import math
from datetime import date

# Expressions arbitraires dans les f-strings
n = 42
print(f"La factorielle de {n} est {math.factorial(n):,}")
print(f"La racine carrée de {n:.2f} est {math.sqrt(n):.4f}")

# Formatage de nombres
pi = math.pi
print(f"π = {pi:.6f}")         # 6 décimales
print(f"π = {pi:10.4f}")       # largeur 10, 4 décimales
print(f"π = {pi:e}")           # notation scientifique
print(f"42 en binaire : {42:08b}")   # binaire sur 8 bits avec zéros
print(f"42 en hexadécimal : {42:#x}")  # hexadécimal avec préfixe

# Formatage de dates
aujourd_hui = date(2026, 3, 19)
print(f"Aujourd'hui : {aujourd_hui:%A %d %B %Y}")   # format strftime dans f-string
```

```{code-cell} python
# Débogage avec = (Python 3.8+)
x = 42
y = [1, 2, 3]
print(f"{x = }")          # Affiche "x = 42"
print(f"{y = }")          # Affiche "y = [1, 2, 3]"
print(f"{len(y) = }")     # Affiche "len(y) = 3"
print(f"{x * 2 + 1 = }") # Affiche "x * 2 + 1 = 85"

# Alignement et remplissage
for valeur in [1, 10, 100, 1000]:
    print(f"  {valeur:>6,} | {'*' * min(valeur // 100, 20)}")
```

```{code-cell} python
# f-strings multilignes
nom = "Alice"
montant = 1234.56
articles = ["Python avancé", "NumPy", "Pandas"]
facture = f"""
╔══════════════════════════════╗
║  Facture pour : {nom:<12}║
║  Montant : {montant:>18,.2f} €  ║
║  Articles :                  ║
""".rstrip()
for i, art in enumerate(articles, 1):
    facture += f"\n║    {i}. {art:<24}║"
facture += "\n╚══════════════════════════════╝"
print(facture)
```

## L'opérateur walrus (`:=`)

L'**opérateur walrus** (`:=`), introduit en Python 3.8, est l'**opérateur d'assignation dans une expression** (*assignment expression*). Il permet d'assigner une valeur à une variable tout en utilisant cette valeur dans une expression, ce qui évite les calculs redondants et simplifie certains patterns.

```{code-cell} python
# Sans walrus : calcul redondant
données = [1, 5, 3, 8, 2, 9, 4]
résultat_1 = [y for x in données if (y := x**2) > 20]
print(f"Carrés supérieurs à 20 : {résultat_1}")

# Dans une boucle while — pattern classique
import io
flux = io.StringIO("ligne 1\nligne 2\nligne 3\n")

lignes = []
while ligne := flux.readline():
    lignes.append(ligne.rstrip())
print(f"Lignes lues : {lignes}")

# Éviter un double appel de fonction
import re
texte = "Réunion le 2026-03-19 à 14h30"
if correspondance := re.search(r"\d{4}-\d{2}-\d{2}", texte):
    print(f"Date trouvée : {correspondance.group()}")
```

```{prf:remark}
:label: remark-21-02
L'opérateur walrus doit être utilisé avec parcimonie. Il est particulièrement adapté aux boucles `while` qui lisent des données, et aux cas où une expression coûteuse est à la fois testée et utilisée. En revanche, l'employer dans des compréhensions complexes ou dans des conditions imbriquées peut nuire à la lisibilité — exactement ce que le Zen de Python cherche à éviter.
```

## `match` / `case` — filtrage par motif

Introduit en Python 3.10, le **filtrage par motif structurel** (`match`/`case`) est bien plus qu'un `switch`/`case` traditionnel. Il permet de déstructurer des objets, des tuples, des dictionnaires et des classes directement dans les branches, en combinant tests de type, d'égalité et d'attributs.

```{code-cell} python
def décrire_forme(forme) -> str:
    match forme:
        case {"type": "cercle", "rayon": r} if r > 0:
            return f"Cercle de rayon {r} (surface : {3.14159 * r**2:.2f})"
        case {"type": "rectangle", "largeur": l, "hauteur": h}:
            return f"Rectangle {l}×{h} (surface : {l * h})"
        case {"type": "triangle", "base": b, "hauteur": h}:
            return f"Triangle base={b}, hauteur={h} (surface : {b * h / 2:.2f})"
        case {"type": type_inconnu}:
            return f"Forme inconnue : {type_inconnu!r}"
        case _:
            return "Objet non reconnu"


formes = [
    {"type": "cercle", "rayon": 5},
    {"type": "rectangle", "largeur": 4, "hauteur": 6},
    {"type": "triangle", "base": 3, "hauteur": 8},
    {"type": "pentagone"},
    42,
]
for f in formes:
    print(décrire_forme(f))
```

```{code-cell} python
# match sur des tuples et des valeurs littérales
def interpréter_commande(commande: tuple) -> str:
    match commande:
        case ("quitter",):
            return "Au revoir !"
        case ("aller", direction) if direction in ("nord", "sud", "est", "ouest"):
            return f"Vous allez vers le {direction}."
        case ("prendre", objet):
            return f"Vous prenez {objet!r}."
        case ("attaquer", cible, arme):
            return f"Vous attaquez {cible!r} avec {arme!r}."
        case _:
            return f"Commande inconnue : {commande}"


commandes = [
    ("aller", "nord"),
    ("prendre", "épée"),
    ("attaquer", "dragon", "lance"),
    ("quitter",),
    ("voler", "vers", "le", "soleil"),
]
for cmd in commandes:
    print(interpréter_commande(cmd))
```

## `pathlib` — manipulation de chemins

Le module `pathlib` (Python 3.4+) propose une API orientée objet pour la manipulation des chemins de fichiers, bien supérieure à la combinaison `os` + `os.path` qu'elle remplace avantageusement.

```{code-cell} python
from pathlib import Path

# Création et composition de chemins
chemin = Path("/tmp") / "projets" / "mon_projet"
print(f"Chemin    : {chemin}")
print(f"Parent    : {chemin.parent}")
print(f"Nom       : {chemin.name}")
print(f"Suffixe   : {chemin.suffix}")
print(f"Suffixes  : {chemin.suffixes}")
print(f"Racine    : {chemin.root}")
print(f"Existe    : {chemin.exists()}")

# Chemin vers le fichier courant (dans un vrai module)
# dossier_courant = Path(__file__).parent
# ressource = dossier_courant / "data" / "config.json"

# Opérations courantes
répertoire_home = Path.home()
print(f"\nRépertoire personnel : {répertoire_home}")
print(f"Chemin absolu de / : {Path('/').absolute()}")

# Itération sur un répertoire
import tempfile, os

with tempfile.TemporaryDirectory() as tmpdir:
    tmp = Path(tmpdir)
    (tmp / "a.py").write_text("# module a")
    (tmp / "b.py").write_text("# module b")
    (tmp / "notes.txt").write_text("notes")

    print(f"\nFichiers dans {tmp.name} :")
    for fichier in sorted(tmp.iterdir()):
        print(f"  {fichier.name} ({fichier.stat().st_size} octets)")

    print("\nFichiers .py uniquement :")
    for py in sorted(tmp.glob("*.py")):
        print(f"  {py.name}")
```

## `collections` — structures de données spécialisées

Le module `collections` offre des structures de données complémentaires aux types intégrés, souvent plus expressives et plus efficaces pour des besoins spécifiques.

```{code-cell} python
from collections import Counter, defaultdict, namedtuple, deque, OrderedDict

# ─── Counter : compter les occurrences ───
texte = "the quick brown fox jumps over the lazy dog"
compte_lettres = Counter(texte.replace(" ", ""))
print("10 lettres les plus fréquentes :")
print(compte_lettres.most_common(10))

# Opérations arithmétiques sur les Counter
c1 = Counter(python=5, java=3, rust=2)
c2 = Counter(python=2, go=4, rust=1)
print(f"\nUnion      : {c1 + c2}")
print(f"Soustraction : {c1 - c2}")
print(f"Intersection : {c1 & c2}")
```

```{code-cell} python
# ─── defaultdict : valeur par défaut automatique ───
# Sans defaultdict : pattern lourd
mots_par_longueur_lourd: dict = {}
mots = ["python", "est", "un", "langage", "élégant"]
for mot in mots:
    longueur = len(mot)
    if longueur not in mots_par_longueur_lourd:
        mots_par_longueur_lourd[longueur] = []
    mots_par_longueur_lourd[longueur].append(mot)

# Avec defaultdict : idiomatique et concis
mots_par_longueur = defaultdict(list)
for mot in mots:
    mots_par_longueur[len(mot)].append(mot)

print("Mots groupés par longueur :")
for longueur, groupe in sorted(mots_par_longueur.items()):
    print(f"  {longueur} lettres : {groupe}")
```

```{code-cell} python
# ─── namedtuple : tuple avec champs nommés ───
# (Préférer dataclass pour les cas plus complexes)
Point = namedtuple("Point", ["x", "y"])
Couleur = namedtuple("Couleur", ["rouge", "vert", "bleu"])

p = Point(3.0, 4.5)
rouge = Couleur(255, 0, 0)

print(f"Point : {p}, x={p.x}, y={p.y}")
print(f"Distance à l'origine : {(p.x**2 + p.y**2)**0.5:.3f}")
print(f"Couleur rouge : {rouge}")
print(f"Composante rouge : {rouge.rouge}")
# Les namedtuples sont toujours des tuples (immuables, décompressables)
x, y = p
print(f"Décompressé : x={x}, y={y}")
```

```{code-cell} python
# ─── deque : file double-terminée ───
from collections import deque

historique = deque(maxlen=5)   # Garde seulement les 5 derniers éléments
for i in range(8):
    historique.append(f"page_{i}")
    print(f"Après append page_{i} : {list(historique)}")
```

## `contextlib` — outils pour les gestionnaires de contexte

Le module `contextlib` simplifie la création de gestionnaires de contexte grâce à `contextmanager`, qui transforme un générateur en gestionnaire de contexte utilisable avec `with`.

```{code-cell} python
from contextlib import contextmanager, suppress, redirect_stdout
import io

# ─── contextmanager : créer un gestionnaire depuis un générateur ───
@contextmanager
def chronomètre(description: str = "opération"):
    """Mesure la durée d'un bloc de code."""
    import time
    début = time.perf_counter()
    try:
        yield
    finally:
        durée = time.perf_counter() - début
        print(f"'{description}' : {durée * 1000:.3f} ms")


with chronomètre("calcul de somme"):
    total = sum(range(1_000_000))
print(f"Total : {total:,}")


# ─── suppress : ignorer des exceptions spécifiques ───
with suppress(FileNotFoundError):
    import os
    os.remove("/tmp/fichier_inexistant_abc.txt")
print("Aucune erreur levée même si le fichier n'existait pas")


# ─── redirect_stdout : capturer la sortie standard ───
sortie = io.StringIO()
with redirect_stdout(sortie):
    print("Ce message est capturé")
    print("Celui-ci aussi")
contenu = sortie.getvalue()
print(f"Sortie capturée : {contenu!r}")
```

```{code-cell} python
# ─── contextmanager pour gérer des ressources ───
@contextmanager
def transaction(connexion_simulée: list):
    """Gestionnaire de transaction avec commit/rollback automatique."""
    sauvegarde = connexion_simulée.copy()
    try:
        yield connexion_simulée
        print("COMMIT : transaction validée")
    except Exception as e:
        connexion_simulée.clear()
        connexion_simulée.extend(sauvegarde)
        print(f"ROLLBACK : transaction annulée ({e})")
        raise


données = [1, 2, 3]
with transaction(données) as db:
    db.append(4)
    db.append(5)
print(f"Après commit : {données}")

try:
    with transaction(données) as db:
        db.append(99)
        raise ValueError("Erreur simulée")
except ValueError:
    pass
print(f"Après rollback : {données}")
```

## Anti-patterns courants

Connaître les idiomes positifs ne suffit pas : il faut aussi connaître les pièges à éviter.

```{prf:example} Anti-patterns fréquents et leurs corrections
:label: example-21-01
Voici les erreurs les plus fréquemment rencontrées dans du code Python non idiomatique.

```python
# ✗ Comparer avec True/False de façon explicite
if ma_liste == True:   ...   # Jamais
if len(ma_liste) > 0: ...   # Inutilement verbeux

# ✓ Pythonique : tirer parti de la vérité implicite
if ma_liste: ...

# ✗ Construire une liste pour itérer immédiatement dessus
for x in list(range(1_000_000)):   # list() inutile
    ...

# ✓ range est déjà un itérable
for x in range(1_000_000):
    ...

# ✗ Concaténation de chaînes en boucle (O(n²) en mémoire)
résultat = ""
for mot in mots:
    résultat += mot + " "

# ✓ join() est la bonne approche
résultat = " ".join(mots)

# ✗ Attraper Exception de façon trop large
try:
    résultat = 1 / x
except Exception:
    pass   # Avale silencieusement toutes les erreurs !

# ✓ Attraper spécifiquement ce que l'on sait gérer
try:
    résultat = 1 / x
except ZeroDivisionError:
    résultat = 0.0
```
```

```{code-cell} python
# Anti-patterns en action — comparaisons

# ✗ Comparer à None avec ==
def anti_pattern_none(x):
    if x == None: return "nul"   # Wrong
    return "non nul"

# ✓ Comparer à None avec is / is not
def idiomatique_none(x):
    if x is None: return "nul"
    return "non nul"

# ✗ Utiliser range(len(...)) pour itérer
fruits = ["pomme", "poire", "cerise"]
for i in range(len(fruits)):              # Non idiomatique
    print(f"  {i}: {fruits[i]}")

# ✓ Utiliser enumerate
for i, fruit in enumerate(fruits):        # Idiomatique
    print(f"  {i}: {fruit}")

# ✗ Construire un dict depuis deux listes avec une boucle
noms_anti = ["a", "b", "c"]
vals_anti = [1, 2, 3]
d_anti = {}
for i in range(len(noms_anti)):
    d_anti[noms_anti[i]] = vals_anti[i]

# ✓ Utiliser dict(zip(...))
d_idiom = dict(zip(noms_anti, vals_anti))
print(d_anti, d_idiom)   # Même résultat
```

```{code-cell} python
# Tirer parti de la vérité implicite des conteneurs
def compter_pairs(nombres):
    """Compte les pairs, retourne 0 si la liste est vide."""
    if not nombres:    # Vérifie si la liste est vide — pythonique
        return 0
    return sum(1 for x in nombres if x % 2 == 0)

print(compter_pairs([]))
print(compter_pairs([1, 2, 3, 4, 5]))

# Utiliser get() sur les dictionnaires plutôt que tester la clé
config = {"debug": True, "port": 8080}

# ✗ Lourd
if "timeout" in config:
    timeout = config["timeout"]
else:
    timeout = 30

# ✓ Idiomatique
timeout = config.get("timeout", 30)
print(f"timeout = {timeout}")

# setdefault : initialiser une clé seulement si elle n'existe pas
groupes: dict = {}
for item in ["a", "b", "a", "c", "b", "a"]:
    groupes.setdefault(item, []).append(1)
comptages = {k: sum(v) for k, v in groupes.items()}
print(comptages)
```

## Visualisation des idiomes pythoniques

```{code-cell} python
:tags: [hide-input]

fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis('off')
ax.set_title("Idiomes pythoniques — carte de référence", fontsize=14,
             fontweight='bold', pad=15)

palette = sns.color_palette("Set2", 6)

catégories = [
    {
        "titre": "Nommage",
        "couleur": palette[0],
        "pos": (0.2, 6.0),
        "idiomes": [
            "snake_case pour variables / fonctions",
            "PascalCase pour les classes",
            "SCREAMING_SNAKE pour les constantes",
            "_privé, __dunder__",
        ]
    },
    {
        "titre": "Compréhensions",
        "couleur": palette[1],
        "pos": (3.6, 6.0),
        "idiomes": [
            "[x for x in it if cond]",
            "{k: v for k, v in d.items()}",
            "{x for x in it}",
            "(x for x in it)  ← générateur",
        ]
    },
    {
        "titre": "Unpacking",
        "couleur": palette[2],
        "pos": (7.0, 6.0),
        "idiomes": [
            "a, b = b, a  ← échange",
            "tête, *reste = liste",
            "for i, v in enumerate(it)",
            "for x, y in zip(a, b)",
        ]
    },
    {
        "titre": "Vérité implicite",
        "couleur": palette[3],
        "pos": (10.4, 6.0),
        "idiomes": [
            "if liste: ...  (pas len())",
            "x is None / is not None",
            "d.get(clé, défaut)",
            "d.setdefault(clé, [])",
        ]
    },
    {
        "titre": "f-strings & walrus",
        "couleur": palette[4],
        "pos": (0.2, 1.8),
        "idiomes": [
            'f"{val:.2f}"  ← formatage',
            'f"{val = }"   ← débogage',
            "if m := re.search(...)",
            "while ligne := f.readline()",
        ]
    },
    {
        "titre": "Outils stdlib",
        "couleur": palette[5],
        "pos": (3.6, 1.8),
        "idiomes": [
            "collections.Counter",
            "collections.defaultdict",
            "pathlib.Path",
            "contextlib.contextmanager",
        ]
    },
]

for cat in catégories:
    x0, y0 = cat["pos"]
    w, h = 3.2, 3.8
    col = cat["couleur"]

    box = patches.FancyBboxPatch((x0, y0), w, h,
        boxstyle="round,pad=0.12", linewidth=2,
        edgecolor=col, facecolor=col, alpha=0.12)
    ax.add_patch(box)
    brd = patches.FancyBboxPatch((x0, y0), w, h,
        boxstyle="round,pad=0.12", linewidth=2,
        edgecolor=col, facecolor='none')
    ax.add_patch(brd)

    # Entête
    hdr = patches.FancyBboxPatch((x0, y0 + h - 0.5), w, 0.5,
        boxstyle="round,pad=0.05", linewidth=0,
        facecolor=col, alpha=0.6)
    ax.add_patch(hdr)
    ax.text(x0 + w / 2, y0 + h - 0.25, cat["titre"],
            ha='center', va='center', fontsize=10,
            fontweight='bold', color='white')

    for j, idiome in enumerate(cat["idiomes"]):
        ax.text(x0 + 0.15, y0 + h - 0.75 - j * 0.68, idiome,
                ha='left', va='center', fontsize=8.2,
                fontfamily='monospace', color='#2a2a2a')

plt.tight_layout()
plt.show()
```

## Résumé

Ce dernier chapitre de synthèse a rassemblé les pratiques et idiomes qui définissent le style pythonique :

- Le **Zen de Python** (`import this`) est la philosophie de conception du langage : clarté, explicitation, simplicité et une seule façon évidente de faire les choses.
- Les **conventions PEP 8** définissent `snake_case` pour les fonctions et variables, `PascalCase` pour les classes, `SCREAMING_SNAKE_CASE` pour les constantes, et les préfixes `_` et `__` pour signaler les détails d'implémentation.
- Les **compréhensions** de listes, de dictionnaires et d'ensembles sont le moyen idiomatique de construire des collections par transformation et filtrage. Les expressions génératrices (`(...)`) offrent la même expressivité en mode paresseux.
- L'**unpacking** distribue les éléments d'un itérable dans plusieurs variables, simplifie les échanges de valeurs, et s'intègre naturellement dans les boucles avec `enumerate` et `zip`.
- Les **f-strings** sont le mécanisme standard de formatage : elles supportent les spécifications de format, le débogage avec `=`, et toute expression Python arbitraire. L'**opérateur walrus** (`:=`) évite les calculs redondants dans les conditions et les boucles.
- L'instruction **`match`/`case`** permet un filtrage structurel puissant sur des tuples, des dictionnaires, des types et des valeurs littérales.
- **`pathlib.Path`** remplace avantageusement `os.path` avec une API orientée objet pour la manipulation des chemins.
- Le module **`collections`** fournit `Counter` (comptages), `defaultdict` (valeur par défaut automatique), `namedtuple` (tuples avec champs nommés) et `deque` (file double-terminée avec taille maximale optionnelle).
- Le module **`contextlib`** permet de créer des gestionnaires de contexte avec `@contextmanager`, et propose `suppress` et `redirect_stdout` pour des besoins courants.
- Les **anti-patterns** à éviter incluent : la comparaison explicite à `True`/`False` ou à `None` avec `==`, la construction de listes inutiles avant itération, la concaténation de chaînes en boucle, et la capture trop large des exceptions.
