El match de Python no es un switch con mejor sintaxis. Es un mecanismo de deconstrucción estructural: en lugar de comparar valores, descompones objetos según su forma, y el runtime vincula nombres a las partes que te interesan. La distinción es fundamental. Un switch evalúa si x == constante. El match evalúa si x tiene la forma de un patrón, y si la tiene, extrae sus componentes en variables locales.
Esto viene directamente de los lenguajes de la familia ML (Haskell, Rust, OCaml), donde el pattern matching es una herramienta central. Python lo adoptó en 3.10 con la PEP 634, y lo hizo con una característica que lo hace especialmente interesante: el protocolo __match_args__, que permite que tus propias clases participen en el matching de forma idiomática.
Hay un gotcha que te va a picar si no lo conoces de entrada: en un case, un nombre suelto como case status no compara con una variable llamada status. Lo captura. Para comparar con una constante existente, necesitas calificarla con un punto (case Status.ACTIVE) o usar un literal. Sin este detalle, un patrón que crees que filtra en realidad acepta todo y sobreescribe tu variable.
Cuando usarlo: parseo de mensajes/eventos, implementación de intérpretes simples, procesamiento de respuestas de APIs donde la estructura varía, y cualquier lugar donde hoy tienes un if/elif largo con isinstance y accesos a diccionarios anidados. Si tu código tiene if isinstance(x, A): ... elif isinstance(x, B): ... combinado con desempaquetado, match lo hace más legible y más seguro.
from dataclasses import dataclass
from typing import Any
# --- Tipos de dominio ---
@dataclass
class Point:
x: float
y: float
__match_args__ = ("x", "y") # Protocolo para matching posicional
@dataclass
class Circle:
center: Point
radius: float
__match_args__ = ("center", "radius")
@dataclass
class Rectangle:
top_left: Point
bottom_right: Point
__match_args__ = ("top_left", "bottom_right")
# --- Comandos como dicts (estilo mensaje de evento / JSON deserializado) ---
def process_command(cmd: dict[str, Any]) -> str:
match cmd:
case {"action": "move", "x": int(x), "y": int(y)}:
# Mapping pattern: extrae "action", "x", "y"
# int(x) es un class pattern: acepta solo si el valor es int, y lo captura en x
return f"Mover a ({x}, {y})"
case {"action": "move", **rest}:
# **rest captura las claves que no matchearon arriba
return f"Comando move malformado, datos extra: {rest}"
case {"action": "resize", "factor": float(f) | int(f)} if f > 0:
# OR pattern: acepta float o int, ambos capturan en f
# guard: la condición extra que debe cumplirse
return f"Redimensionar x{f}"
case {"action": "resize"}:
return "resize sin factor válido"
case {"action": str(action)}:
# Captura cualquier acción desconocida de tipo str
return f"Acción desconocida: {action!r}"
case _:
# Wildcard: acepta cualquier cosa, no vincula nombre
return "Comando no reconocido"
# --- Matching sobre secuencias ---
def parse_path(parts: list[str]) -> str:
match parts:
case []:
return "Raíz"
case ["users", user_id]:
# Secuencia exacta de dos elementos
return f"Usuario: {user_id}"
case ["users", user_id, "posts", post_id]:
return f"Post {post_id} del usuario {user_id}"
case ["users", user_id, *rest]:
# *rest captura el tail, como el * del unpacking normal
return f"Sub-recurso de {user_id}: {'/'.join(rest)}"
case ["admin", *_]:
# *_ descarta el resto sin vincularlo
return "Ruta de administración"
case [first, *_]:
return f"Ruta desconocida que empieza con {first!r}"
# --- Matching sobre objetos con __match_args__ ---
def describe_shape(shape: Any) -> str:
match shape:
case Circle(center=Point(x=0, y=0), radius=r):
# Matching posicional anidado: Circle cuyo centro es exactamente (0,0)
return f"Círculo en el origen, radio {r}"
case Circle(center=Point(x=cx, y=cy), radius=r) if r > 100:
# Guard sobre un objeto extraído del match
return f"Círculo grande en ({cx},{cy}), radio {r}"
case Circle(center=Point(x=cx, y=cy), radius=r):
return f"Círculo en ({cx},{cy}), radio {r}"
case Rectangle(top_left=Point(x=x1, y=y1), bottom_right=Point(x=x2, y=y2)):
width = abs(x2 - x1)
height = abs(y2 - y1)
return f"Rectángulo {width}x{height}"
case _:
return "Forma desconocida"
# --- Demo ---
if __name__ == "__main__":
# Comandos
print(process_command({"action": "move", "x": 10, "y": 20}))
print(process_command({"action": "move", "x": "diez", "y": 20}))
print(process_command({"action": "resize", "factor": 2.5}))
print(process_command({"action": "resize", "factor": -1}))
print(process_command({"action": "delete"}))
print()
# Rutas
print(parse_path([]))
print(parse_path(["users", "42"]))
print(parse_path(["users", "42", "posts", "7"]))
print(parse_path(["users", "42", "settings", "email"]))
print(parse_path(["admin", "dashboard"]))
print()
# Formas
print(describe_shape(Circle(Point(0, 0), 5)))
print(describe_shape(Circle(Point(3, 4), 150)))
print(describe_shape(Circle(Point(1, 2), 10)))
print(describe_shape(Rectangle(Point(0, 0), Point(8, 6))))
Lo que está pasando realmente
Los patrones de mapping ({"action": "move", "x": int(x)}) no requieren que el dict tenga exactamente esas claves: es una coincidencia por subconjunto. Si el dict tiene más claves, el patrón sigue matcheando. Por eso el orden entre case {"action": "move", "x": int(x), "y": int(y)} y case {"action": "move", **rest} importa: el primero es más específico y debe ir antes.
Los class patterns (int(x)) son más sutiles de lo que parecen. int(x) no llama a int(x). Le pregunta al objeto si es instancia de int y, si lo es, extrae su valor usando __match_args__. Para los tipos builtin (int, str, float, bool), Python tiene reglas especiales: el primer argumento posicional captura el valor mismo. Para tus propias clases, __match_args__ define el orden de los atributos posicionales, lo que permite Circle(Point(0, 0), 5) en lugar de tener que escribir Circle(center=Point(x=0, y=0), radius=5).
Los guards (if f > 0) se evalúan solo cuando el patrón ya matcheó. No son parte del patrón; son una condición adicional. Si el guard falla, Python sigue buscando el siguiente case. Esto es importante: un patrón con guard fallido no “bloquea” los casos siguientes.
El OR pattern (float(f) | int(f)) tiene una restricción: ambas alternativas deben vincular exactamente los mismos nombres. float(f) | int(g) es un error de sintaxis porque las ramas no son simétricas en sus capturas. Python lo detecta en tiempo de compilación.
Las secuencias matchean contra cualquier cosa que sea secuencia (listas, tuplas, cualquier objeto con __len__ y __getitem__), excepto strings y bytes, que se excluyen deliberadamente para evitar confusión.
Errores que debes conocer
Error: usar un nombre de variable en el case esperando comparar con su valor, cuando en realidad el nombre actúa como patrón de captura.
# ❌ Wrong
STATUS_OK = 200
def handle(code):
match code:
case STATUS_OK: # No compara con 200. Captura cualquier valor en STATUS_OK.
return "ok" # Este case siempre matchea y sobreescribe STATUS_OK.
case _:
return "error"
# ✅ Right
from http import HTTPStatus
def handle(code):
match code:
case 200: # Literal: sí compara
return "ok"
case HTTPStatus.NOT_FOUND: # Dotted name: sí compara
return "not found"
case _:
return "error"
Los nombres simples en un case siempre son capturas. Solo los literales y los nombres con punto actúan como comparaciones.
Error: asumir que el orden de los case no importa cuando usas mapping patterns sin **rest.
# ❌ Wrong
def route(cmd):
match cmd:
case {"action": str(a)}: # Demasiado general, primero
return f"genérico: {a}"
case {"action": "move", "x": int(x), "y": int(y)}: # Nunca llega aquí
return f"mover a ({x},{y})"
# ✅ Right
def route(cmd):
match cmd:
case {"action": "move", "x": int(x), "y": int(y)}: # Más específico, primero
return f"mover a ({x},{y})"
case {"action": str(a)}:
return f"genérico: {a}"
El runtime evalúa los case en orden y ejecuta el primero que matchee. Los patrones más específicos siempre van antes que los generales, igual que en un try/except con jerarquías de excepciones.
N° 49