39  Pattern matching avec match / case

Python 3.10 (octobre 2021) a introduit l’instruction match/case — une forme évoluée de « switch » bien plus puissante que dans d’autres langages. Elle permet non seulement de comparer des valeurs, mais aussi de destructurer des collections, de filtrer par type, et de capturer des parties des données. Le TOSA Avancé la teste depuis 2023.

39.1 Pourquoi match / case ?

Imaginez traiter un message sous différentes formes :

# Avec if/elif
def traiter(msg):
    if isinstance(msg, str):
        print(f"Texte : {msg}")
    elif isinstance(msg, int):
        print(f"Nombre : {msg}")
    elif isinstance(msg, list) and len(msg) == 2:
        print(f"Coordonnées : {msg[0]}, {msg[1]}")
    elif isinstance(msg, dict) and "type" in msg:
        print(f"Message typé : {msg['type']}")
    else:
        print("Inconnu")

traiter("Hello")
traiter(42)
traiter([3, 4])
traiter({"type": "error", "code": 500})
Texte : Hello
Nombre : 42
Coordonnées : 3, 4
Message typé : error

La même chose avec match/case :

def traiter(msg):
    match msg:
        case str(s):
            print(f"Texte : {s}")
        case int(n):
            print(f"Nombre : {n}")
        case [x, y]:
            print(f"Coordonnées : {x}, {y}")
        case {"type": t}:
            print(f"Message typé : {t}")
        case _:
            print("Inconnu")

traiter("Hello")
traiter(42)
traiter([3, 4])
traiter({"type": "error", "code": 500})
Texte : Hello
Nombre : 42
Coordonnées : 3, 4
Message typé : error

Plus concis, plus lisible, et surtout : on capture directement les parties intéressantes dans des variables.

39.2 Syntaxe de base

match expression:
    case motif1:
        # code si expression correspond à motif1
    case motif2:
        # code si expression correspond à motif2
    case _:
        # cas par défaut (wildcard)

Comme pour if/elif, le premier case qui correspond est exécuté — un seul.

Le wildcard _

_ (un simple underscore) est un motif spécial qui correspond à tout. On l’utilise comme cas par défaut.

def jour_semaine(n):
    match n:
        case 1: return "Lundi"
        case 2: return "Mardi"
        case 3: return "Mercredi"
        case 4: return "Jeudi"
        case 5: return "Vendredi"
        case 6: return "Samedi"
        case 7: return "Dimanche"
        case _: return "Jour invalide"

for n in [1, 3, 7, 99]:
    print(f"{n}{jour_semaine(n)}")
1 → Lundi
3 → Mercredi
7 → Dimanche
99 → Jour invalide
Et si aucun case ne correspond ?

Si aucun motif ne correspond et qu’aucun _ n’est présent, match ne fait rien (contrairement à un if/elif où ce serait pareil). Pas d’erreur, mais le bloc est simplement ignoré.

Bonne pratique : toujours inclure un case _: pour traiter explicitement le cas imprévu.

39.3 Motifs littéraux

Les plus simples : comparer à une valeur littérale.

def reponse(code):
    match code:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500:
            return "Server Error"
        case _:
            return f"Code {code}"

print(reponse(200))
print(reponse(404))
print(reponse(418))
OK
Not Found
Code 418

Alternatives avec |

Pour matcher plusieurs valeurs d’un coup, utilisez | (pipe) :

def categorie_http(code):
    match code:
        case 200 | 201 | 204:
            return "Succès"
        case 301 | 302:
            return "Redirection"
        case 400 | 401 | 403 | 404:
            return "Erreur client"
        case 500 | 502 | 503:
            return "Erreur serveur"
        case _:
            return "Autre"

for code in [200, 301, 404, 502, 418]:
    print(f"{code}{categorie_http(code)}")
200 → Succès
301 → Redirection
404 → Erreur client
502 → Erreur serveur
418 → Autre

39.4 Capture de variables

Un case peut capturer une valeur dans une variable — très pratique.

def decrire(valeur):
    match valeur:
        case 0:
            return "zéro"
        case 1:
            return "un"
        case n:
            return f"autre nombre : {n}"   # n capture la valeur

print(decrire(0))
print(decrire(1))
print(decrire(42))
zéro
un
autre nombre : 42
⚠️ Piège : un nom nu capture TOUJOURS

Un nom en minuscules tout seul dans un case capture la valeur — il ne la compare pas !

MAX = 100

match x:
    case MAX:         # ❌ capture dans MAX, ne compare PAS à 100 !
        print("Maximum atteint")

Pour comparer à une constante, utilisez un nom qualifié (avec un point) :

class Limites:
    MAX = 100

match x:
    case Limites.MAX:    # ✅ comparaison
        print("Maximum")

Ou des littéraux entiers : case 100: marche sans souci.

39.5 Motifs sur les séquences

match peut destructurer une liste ou un tuple.

Nombre fixe d’éléments

def traiter_point(p):
    match p:
        case (0, 0):
            return "Origine"
        case (x, 0):
            return f"Sur l'axe X à {x}"
        case (0, y):
            return f"Sur l'axe Y à {y}"
        case (x, y):
            return f"Point ({x}, {y})"
        case _:
            return "Pas un point 2D"

print(traiter_point((0, 0)))
print(traiter_point((3, 0)))
print(traiter_point((0, 5)))
print(traiter_point((3, 4)))
print(traiter_point((1, 2, 3)))
Origine
Sur l'axe X à 3
Sur l'axe Y à 5
Point (3, 4)
Pas un point 2D

Nombre variable avec *

Le * capture « le reste » dans une liste.

def decouper_liste(liste):
    match liste:
        case []:
            return "Vide"
        case [x]:
            return f"Un seul : {x}"
        case [x, y]:
            return f"Deux : {x}, {y}"
        case [premier, *milieu, dernier]:
            return f"Premier={premier}, dernier={dernier}, milieu={milieu}"
        case _:
            return "Autre"

for l in [[], [1], [1, 2], [1, 2, 3, 4, 5]]:
    print(decouper_liste(l))
Vide
Un seul : 1
Deux : 1, 2
Premier=1, dernier=5, milieu=[2, 3, 4]

Notez que le motif [premier, *milieu, dernier] capture directement la structure — équivalent à vouloir faire liste[0], liste[1:-1], liste[-1] mais en une ligne.

39.6 Motifs sur les dictionnaires

On peut matcher sur certaines clés d’un dict, sans exiger toutes les clés.

def traiter_reponse(rep):
    match rep:
        case {"statut": "ok", "data": data}:
            return f"Données reçues : {data}"
        case {"statut": "erreur", "message": msg}:
            return f"Erreur : {msg}"
        case {"statut": "ok"}:
            return "OK (sans données)"
        case _:
            return "Format inconnu"

print(traiter_reponse({"statut": "ok", "data": [1, 2, 3]}))
print(traiter_reponse({"statut": "erreur", "message": "Forbidden"}))
print(traiter_reponse({"statut": "ok", "meta": "..."}))   # match le 3e : statut=ok
print(traiter_reponse({"autre": "chose"}))
Données reçues : [1, 2, 3]
Erreur : Forbidden
OK (sans données)
Format inconnu
Le match de dict est partiel

Le motif {"statut": "ok"} correspond à tout dict contenant au moins cette clé. Le dict peut avoir d’autres clés — elles sont ignorées. C’est différent d’une comparaison d’égalité.

39.7 Motifs par type (class patterns)

C’est la fonctionnalité la plus puissante du pattern matching. On peut matcher sur le type d’un objet et capturer ses attributs.

Type simple

def identifier(x):
    match x:
        case int():
            return f"Entier : {x}"
        case float():
            return f"Flottant : {x}"
        case str():
            return f"Chaîne : '{x}'"
        case list():
            return f"Liste de {len(x)} éléments"
        case _:
            return "Type inconnu"

for v in [42, 3.14, "hello", [1, 2, 3], (1, 2)]:
    print(identifier(v))
Entier : 42
Flottant : 3.14
Chaîne : 'hello'
Liste de 3 éléments
Type inconnu

Les parenthèses () après le nom de classe sont obligatoires — c’est une syntaxe spécifique au pattern matching.

Type + capture

On peut à la fois matcher sur le type et capturer la valeur :

def doubler(x):
    match x:
        case int(n):
            return n * 2
        case str(s):
            return s + s
        case list(l):
            return l + l
        case _:
            return None

print(doubler(5))
print(doubler("abc"))
print(doubler([1, 2]))
10
abcabc
[1, 2, 1, 2]

Avec des classes personnelles : attributs

Pour les classes POO, on peut matcher sur leurs attributs.

class Point:
    __match_args__ = ("x", "y")    # ordre des attributs pour le match positionnel

    def __init__(self, x, y):
        self.x = x
        self.y = y


def position(p):
    match p:
        case Point(0, 0):
            return "Origine"
        case Point(x=0, y=y):
            return f"Sur axe Y en y={y}"
        case Point(x=x, y=0):
            return f"Sur axe X en x={x}"
        case Point(x, y):              # capture positionnelle grâce à __match_args__
            return f"Point ({x}, {y})"

print(position(Point(0, 0)))
print(position(Point(0, 5)))
print(position(Point(3, 0)))
print(position(Point(3, 4)))
Origine
Sur axe Y en y=5
Sur axe X en x=3
Point (3, 4)
__match_args__

L’attribut spécial __match_args__ définit l’ordre des paramètres pour le match positionnel. Sans lui, seul le match par nom (Point(x=..., y=...)) fonctionne.

Avec @dataclass, __match_args__ est généré automatiquement d’après l’ordre des champs :

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p = Point(3, 4)
match p:
    case Point(x, y):          # ça marche directement
        print(f"({x}, {y})")
(3, 4)

39.8 Clauses if (gardes)

On peut ajouter une condition supplémentaire à un case avec if.

def categoriser(age):
    match age:
        case int(n) if n < 0:
            return "Âge invalide"
        case int(n) if n < 18:
            return "Mineur"
        case int(n) if n < 65:
            return "Adulte"
        case int(n):
            return "Senior"
        case _:
            return "Pas un âge valide"

for age in [-5, 10, 30, 75, "trente"]:
    print(f"{age}{categoriser(age)}")
-5 → Âge invalide
10 → Mineur
30 → Adulte
75 → Senior
trente → Pas un âge valide

Les gardes sont très puissantes combinées avec la capture — on filtre finement sans imbriquer des if.

39.9 Récapitulatif des motifs

Type de motif Exemple Effet
Littéral case 42: Comparaison d’égalité
Wildcard case _: N’importe quoi
Capture case n: Capture dans n
Alternative case 1 \| 2 \| 3: L’un des trois
Séquence case [a, b]: Liste/tuple de 2 éléments
Séquence + * case [x, *rest]: Déballage
Dict case {"k": v}: Dict contenant au moins cette clé
Type case str(): Instance de la classe
Type + attributs case Point(x=0): Type + valeurs d’attributs
Garde case n if n > 0: Condition supplémentaire

39.10 match vs if/elif : quand choisir ?

Guide pratique
  • match/case brille quand vous :
    • Comparez une valeur à plusieurs formes (nombre, chaîne, liste, dict).
    • Voulez destructurer et capturer en même temps.
    • Matchez sur types d’objets (polymorphisme).
    • Avez plus de 2-3 branches qui partagent un même sujet.
  • if/elif reste préférable quand :
    • Les conditions sont indépendantes (pas un même sujet à matcher).
    • Vous ne faites qu’une comparaison simple entre deux valeurs.
    • Vous avez besoin de Python < 3.10 (support legacy).

39.11 Attention : rétrocompatibilité

match/case est disponible à partir de Python 3.10. Il ne fonctionne pas en Python 3.9 ou antérieur.

# Python 3.9 : SyntaxError
match x:
    case 1: ...

Si votre code doit fonctionner sur des versions plus anciennes, utilisez if/elif/else.


🧩 Quiz 10.1 — Pattern matching

Question 1

À partir de quelle version de Python match/case existe-t-il ?

  1. 3.7
  2. 3.8
  3. 3.9
  4. 3.10

d) 3.10 — octobre 2021. C’est la version de référence du TOSA.

Question 2

Que fait case _: ?

  1. Ne fait rien
  2. Capture la valeur dans une variable _
  3. Match n’importe quelle valeur (cas par défaut)
  4. Termine le match

c)_ est un wildcard, il matche toujours. Équivalent du default: dans d’autres langages.

Question 3

Que s’affiche-t-il ?

x = 5
match x:
    case 1 | 2 | 3:
        print("petit")
    case 4 | 5 | 6:
        print("moyen")
    case _:
        print("grand")
  1. petit
  2. moyen
  3. grand
  4. petit moyen

b) moyen — le | permet de combiner plusieurs valeurs dans un seul case.

x = 5
match x:
    case 1 | 2 | 3:
        print("petit")
    case 4 | 5 | 6:
        print("moyen")
    case _:
        print("grand")
moyen

Question 4

Que vaut resultat ?

valeur = [1, 2, 3, 4, 5]
match valeur:
    case [x, *middle, y]:
        resultat = (x, middle, y)
  1. (1, [2, 3, 4], 5)
  2. (1, 2, 5)
  3. (1, [2, 3], 5)
  4. Une erreur

a) (1, [2, 3, 4], 5)*middle capture tous les éléments entre le premier et le dernier.

valeur = [1, 2, 3, 4, 5]
match valeur:
    case [x, *middle, y]:
        print((x, middle, y))
(1, [2, 3, 4], 5)

Question 5

Que s’affiche-t-il ?

msg = {"statut": "ok", "data": 42}
match msg:
    case {"statut": "ok", "data": d}:
        print(f"OK : {d}")
    case {"statut": "erreur"}:
        print("Erreur")
  1. OK : 42
  2. Erreur
  3. Rien
  4. Une erreur

a) OK : 42 — le premier motif matche le dict et capture data dans d.

msg = {"statut": "ok", "data": 42}
match msg:
    case {"statut": "ok", "data": d}:
        print(f"OK : {d}")
    case {"statut": "erreur"}:
        print("Erreur")
OK : 42

Question 6

Que s’affiche-t-il ?

MAX = 100
x = 50
match x:
    case MAX:
        print("Max atteint")
    case _:
        print(f"Pas encore : {MAX}")
  1. Max atteint
  2. Pas encore : 100
  3. Pas encore : 50
  4. Une erreur

d) Une erreurpiège TOSA : MAX dans un case est un nom nu, donc Python le traite comme une capture (qui matche tout), pas comme une comparaison à 100. Le compilateur détecte statiquement que case _: devient alors inaccessible et refuse de compiler dès Python 3.10 :

SyntaxError: name capture 'MAX' makes remaining patterns unreachable

Pour comparer à la constante, utilisez un nom qualifié (Limites.MAX) ou un littéral (case 100:).

Sans le case _:, le code compile — et on voit bien la capture à l’œuvre : MAX est écrasé par la valeur de x.

MAX = 100
x = 50
match x:
    case MAX:
        print("Max atteint")                     # s'exécute car MAX capture 50
        print(f"MAX vaut maintenant : {MAX}")    # MAX = 50 !
Max atteint
MAX vaut maintenant : 50

Question 7

Comment ajouter une condition supplémentaire à un case ?

  1. case n if n > 0:
  2. case n when n > 0:
  3. case n where n > 0:
  4. case n && n > 0:

a) case n if n > 0: — les gardes utilisent le mot-clé if.

x = 5
match x:
    case n if n > 0:
        print(f"Positif : {n}")
    case n if n < 0:
        print(f"Négatif : {n}")
    case 0:
        print("Zéro")
Positif : 5

✏️ Exercice 10.1 — Calculatrice par match

Écrivez une fonction calculer(expression) qui reçoit un tuple (op, a, b) et effectue l’opération correspondante.

def calculer(expression):
    match expression:
        case ("+", a, b): ...
        case ("-", a, b): ...
        case ("*", a, b): ...
        case ("/", a, b) if b != 0: ...
        case _: ...

print(calculer(("+", 3, 5)))
print(calculer(("*", 4, 7)))
print(calculer(("/", 10, 2)))
print(calculer(("/", 10, 0)))
print(calculer(("?", 1, 2)))
def calculer(expression):
    match expression:
        case ("+", a, b):
            return a + b
        case ("-", a, b):
            return a - b
        case ("*", a, b):
            return a * b
        case ("/", a, b) if b != 0:
            return a / b
        case ("/", _, 0):
            return "Division par zéro"
        case _:
            return "Opération inconnue"

print(calculer(("+", 3, 5)))
print(calculer(("-", 10, 4)))
print(calculer(("*", 4, 7)))
print(calculer(("/", 10, 2)))
print(calculer(("/", 10, 0)))
print(calculer(("?", 1, 2)))
8
6
28
5.0
Division par zéro
Opération inconnue
  • Le motif ("+", a, b) destructure le tuple et capture a et b en même temps.
  • La garde if b != 0 filtre les divisions légitimes.
  • Le cas ("/", _, 0) gère spécifiquement la division par zéro. Le _ ignore le premier argument (divisé).
  • L’ordre des case compte : les cas plus spécifiques d’abord.

✏️ Exercice 10.2 — Analyse de messages

Écrivez une fonction traiter_message(msg) qui réagit à différents formats de messages dict.

def traiter_message(msg):
    match msg:
        case {"type": "ping"}: ...
        case {"type": "login", "user": u}: ...
        case {"type": "logout"}: ...
        case {"type": "data", "payload": p}: ...
        case _: ...

print(traiter_message({"type": "ping"}))
print(traiter_message({"type": "login", "user": "Alice"}))
print(traiter_message({"type": "data", "payload": [1, 2, 3]}))
print(traiter_message({"type": "inconnu"}))
def traiter_message(msg):
    match msg:
        case {"type": "ping"}:
            return "pong"
        case {"type": "login", "user": user}:
            return f"Bienvenue, {user}"
        case {"type": "logout"}:
            return "À bientôt"
        case {"type": "data", "payload": payload}:
            return f"Reçu {len(payload)} éléments"
        case {"type": type_}:
            return f"Type inconnu : {type_}"
        case _:
            return "Message mal formé"


for msg in [
    {"type": "ping"},
    {"type": "login", "user": "Alice"},
    {"type": "logout"},
    {"type": "data", "payload": [1, 2, 3, 4]},
    {"type": "inconnu"},
    {"autre": "chose"},
]:
    print(f"{msg}{traiter_message(msg)}")
{'type': 'ping'} → pong
{'type': 'login', 'user': 'Alice'} → Bienvenue, Alice
{'type': 'logout'} → À bientôt
{'type': 'data', 'payload': [1, 2, 3, 4]} → Reçu 4 éléments
{'type': 'inconnu'} → Type inconnu : inconnu
{'autre': 'chose'} → Message mal formé

✏️ Exercice 10.3 — Classifier des formes géométriques

Créez deux @dataclass Cercle(rayon) et Rectangle(longueur, largeur). Écrivez aire(forme) avec match qui calcule l’aire selon le type.

from dataclasses import dataclass
from math import pi

@dataclass
class Cercle:
    rayon: float

@dataclass
class Rectangle:
    longueur: float
    largeur: float

def aire(forme):
    match forme:
        case Cercle(r): ...
        case Rectangle(l, w): ...
        case _: ...

print(aire(Cercle(5)))
print(aire(Rectangle(4, 3)))
print(aire("carré"))
from dataclasses import dataclass
from math import pi

@dataclass
class Cercle:
    rayon: float

@dataclass
class Rectangle:
    longueur: float
    largeur: float


def aire(forme):
    match forme:
        case Cercle(r):
            return pi * r ** 2
        case Rectangle(l, w):
            return l * w
        case Rectangle(l, l2) if l == l2:          # carré = rectangle avec côtés égaux
            return l ** 2
        case _:
            raise TypeError(f"Forme inconnue : {forme}")


print(f"Cercle r=5 : {aire(Cercle(5)):.2f}")
print(f"Rectangle 4x3 : {aire(Rectangle(4, 3))}")
print(f"Carré 4x4 : {aire(Rectangle(4, 4))}")
Cercle r=5 : 78.54
Rectangle 4x3 : 12
Carré 4x4 : 16

Grâce à @dataclass, __match_args__ est généré automatiquement, ce qui permet d’écrire Cercle(r) et Rectangle(l, w) sans configuration supplémentaire.

Ajouter le motif Rectangle(l, l2) if l == l2 devient plus spécifique que Rectangle(l, w) — il faut donc le placer avant dans l’ordre des cases.


À retenir

Points clés du chapitre
  1. match/case est disponible depuis Python 3.10.
  2. case _: est le cas par défaut (wildcard).
  3. Littéraux : case 42:. Alternatives : case 1 | 2 | 3:.
  4. Capture : un nom nu capture la valeur (attention aux constantes !).
  5. Séquences : case [x, y]: destructure. case [x, *rest]: capture le reste.
  6. Dicts : case {"clé": valeur}:match partiel sur les clés nommées.
  7. Types : case str():, case Point(x, y): pour classes avec __match_args__.
  8. @dataclass génère automatiquement __match_args__.
  9. Gardes avec if : case n if n > 0:.
  10. Ordre des case : du plus spécifique au plus général.

← Chapitre précédent : ComplexitéTP récapitulatif →