match en Python 3.10: pattern matching estructural con case, guards y clases

Python 3.10 introdujo el match statement, que va mucho más allá de un simple switch/case. Es pattern matching estructural: puede desempaquetar datos, comprobar tipos, extraer valores y añadir condiciones adicionales, todo en una sintaxis limpia que elimina cadenas de if/elif complejas.

La sintaxis básica

# Comparación directa de valores (como switch/case clásico)
def dia_tipo(dia: str) -> str:
    match dia.lower():
        case "lunes" | "martes" | "miércoles" | "jueves" | "viernes":
            return "laborable"
        case "sábado" | "domingo":
            return "fin de semana"
        case _:  # wildcard: coincide con cualquier cosa
            return "día desconocido"

print(dia_tipo("Lunes"))    # laborable
print(dia_tipo("Domingo"))  # fin de semana
print(dia_tipo("festivo"))  # día desconocido

Pattern matching de secuencias

def parsear_comando(args: list[str]) -> str:
    match args:
        case []:
            return "sin argumentos"
        case [comando]:
            return f"comando simple: {comando}"
        case [comando, *resto]:
            return f"comando '{comando}' con {len(resto)} argumentos: {resto}"

print(parsear_comando([]))                     # sin argumentos
print(parsear_comando(["listar"]))             # comando simple: listar
print(parsear_comando(["copiar", "a.txt", "b.txt"]))  # comando 'copiar' con 2 argumentos
# Desempaquetar coordenadas
def describir_punto(punto):
    match punto:
        case (0, 0):
            return "origen"
        case (x, 0):
            return f"en el eje X en {x}"
        case (0, y):
            return f"en el eje Y en {y}"
        case (x, y):
            return f"punto en ({x}, {y})"

print(describir_punto((0, 0)))   # origen
print(describir_punto((5, 0)))   # en el eje X en 5
print(describir_punto((3, 4)))   # punto en (3, 4)

Pattern matching de mappings (dicts)

def procesar_evento(evento: dict) -> str:
    match evento:
        case {"tipo": "click", "x": x, "y": y}:
            return f"Click en ({x}, {y})"
        case {"tipo": "teclado", "tecla": tecla, "modificador": "ctrl"}:
            return f"Ctrl+{tecla.upper()}"
        case {"tipo": "teclado", "tecla": tecla}:
            return f"Tecla: {tecla}"
        case {"tipo": tipo}:
            return f"Evento desconocido: {tipo}"
        case _:
            return "Formato de evento inválido"

print(procesar_evento({"tipo": "click", "x": 100, "y": 200}))
print(procesar_evento({"tipo": "teclado", "tecla": "s", "modificador": "ctrl"}))
print(procesar_evento({"tipo": "teclado", "tecla": "enter"}))

Pattern matching de clases

from dataclasses import dataclass

@dataclass
class Circulo:
    radio: float

@dataclass
class Rectangulo:
    ancho: float
    alto: float

@dataclass
class Triangulo:
    base: float
    altura: float

def calcular_area(figura) -> float:
    import math
    match figura:
        case Circulo(radio=r):
            return math.pi * r ** 2
        case Rectangulo(ancho=w, alto=h):
            return w * h
        case Triangulo(base=b, altura=h):
            return 0.5 * b * h
        case _:
            raise ValueError(f"Figura no reconocida: {type(figura)}")

print(f"{calcular_area(Circulo(5)):.2f}")          # 78.54
print(f"{calcular_area(Rectangulo(4, 6)):.2f}")    # 24.00
print(f"{calcular_area(Triangulo(3, 8)):.2f}")     # 12.00

Guards con if

def clasificar_numero(n: int) -> str:
    match n:
        case 0:
            return "cero"
        case n if n < 0:
            return f"negativo: {n}"
        case n if n % 2 == 0:
            return f"par positivo: {n}"
        case n:
            return f"impar positivo: {n}"

print(clasificar_numero(0))    # cero
print(clasificar_numero(-5))   # negativo: -5
print(clasificar_numero(4))    # par positivo: 4
print(clasificar_numero(7))    # impar positivo: 7

Ejemplo real: parsear respuestas JSON de una API

def manejar_respuesta(respuesta: dict) -> str:
    match respuesta:
        case {"status": 200, "data": {"usuarios": usuarios}} if len(usuarios) > 0:
            return f"Encontrados {len(usuarios)} usuarios"
        case {"status": 200, "data": {"usuarios": []}}:
            return "Lista de usuarios vacía"
        case {"status": 404, "mensaje": msg}:
            return f"No encontrado: {msg}"
        case {"status": codigo} if 500 <= codigo < 600:
            return f"Error de servidor: {codigo}"
        case {"status": codigo}:
            return f"Respuesta inesperada: {codigo}"

print(manejar_respuesta({"status": 200, "data": {"usuarios": ["Ana", "Luis"]}}))
print(manejar_respuesta({"status": 404, "mensaje": "Usuario no existe"}))
print(manejar_respuesta({"status": 503}))

Capturar con as

def procesar(dato):
    match dato:
        case [x, y] as punto if x > 0 and y > 0:
            print(f"Punto en el primer cuadrante: {punto}")
        case [x, y]:
            print(f"Punto fuera del primer cuadrante: ({x}, {y})")
        case str() as s if s.startswith("http"):
            print(f"URL detectada: {s}")
        case _:
            print("Dato no reconocido")

procesar([3, 4])                    # primer cuadrante
procesar([-1, 2])                   # fuera del primer cuadrante
procesar("https://python.org")      # URL detectada

El match no reemplaza a todos los if: brilla cuando la lógica depende de la estructura de los datos (tipo, forma, campos presentes). Para condiciones simples de un solo valor, el if sigue siendo más claro. Pero cuando procesas eventos, resultados de API, comandos CLI o mensajes entre componentes, el match hace el código significativamente más legible.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP