Desempaquetado en Python: más allá del intercambio

Cuando escribes a, b = b, a para intercambiar dos variables, ya estás usando desempaquetado. Pero eso es solo la entrada del túnel — el mecanismo es mucho más general y cambia la forma en que lees y escribes código Python a diario.

El desempaquetado (unpacking) es el proceso por el que Python toma un iterable y distribuye sus elementos en múltiples nombres en una sola instrucción. El lado izquierdo de la asignación define una estructura, y Python la rellena elemento a elemento. Funciona con cualquier iterable: tuplas, listas, cadenas, rangos, generadores. Lo que importa es la forma, no el tipo.

¿Por qué Python lo diseñó así? Porque Python trata los iterables como ciudadanos de primera clase. Una tupla (1, 2, 3) no es un objeto especial que necesitas desmontar con índices — es simplemente una secuencia, y Python puede “expandirla” directamente contra una estructura de nombres. El intérprete hace ese trabajo en tiempo de compilación del bytecode, no en tiempo de ejecución con overhead notable.

Lo usas cuando quieres expresar estructura: extraer partes de un registro, iterar sobre pares clave-valor, pasar una lista como argumentos posicionales. Lo evitas cuando la estructura del iterable no es obvia para quien lee — un a, b, c, d, e = fila sin contexto es un código que pide un accidente.

El error clásico es una desigualdad en el conteo: si el lado derecho tiene más o menos elementos que el lado izquierdo, Python lanza ValueError inmediatamente. No hay comportamiento silencioso ni truncado.

# Ejemplo completo y ejecutable de desempaquetado en Python

from typing import Iterator


# ── 1. Desempaquetado básico de tupla y lista ──────────────────────────
point = (40.4168, -3.7038)
lat, lon = point  # distribuye exactamente 2 elementos en 2 nombres

rgb = [255, 128, 0]
red, green, blue = rgb


# ── 2. Desempaquetado anidado ─────────────────────────────────────────
# Una fila de base de datos: (id, nombre, (ciudad, país))
record = (42, "Ana García", ("Madrid", "ES"))
user_id, name, (city, country) = record  # la estructura interna se respeta


# ── 3. Desempaquetado extendido con * ─────────────────────────────────
temperatures = [18, 21, 19, 23, 20, 17, 22]
first, *middle, last = temperatures   # * captura todo lo que sobra como lista
_, *workdays, _ = temperatures        # _ = convención para "no me importa"

# * puede ir en cualquier posición, pero solo uno por asignación
head, *tail = temperatures            # head = 18, tail = [21, 19, 23, 20, 17, 22]
*init, final = temperatures           # init = [18, 21, 19, 23, 20, 17], final = 22


# ── 4. Desempaquetado en bucles for ───────────────────────────────────
inventory = {
    "manzanas": 50,
    "peras": 30,
    "uvas": 75,
}

for product, stock in inventory.items():  # cada item() devuelve una tupla (k, v)
    if stock < 40:
        print(f"{product}: reponer ({stock} unidades)")

# Con estructuras más ricas: lista de tuplas de 3 elementos
sales = [("manzanas", "2024-01", 1200), ("peras", "2024-01", 800)]
for fruit, month, revenue in sales:
    print(f"{fruit} — {month}: {revenue}€")


# ── 5. Desempaquetado en llamadas a funciones ─────────────────────────
def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
    # Fórmula simplificada solo para ilustrar la llamada
    return ((lat2 - lat1) ** 2 + (lon2 - lon1) ** 2) ** 0.5


madrid = (40.4168, -3.7038)
barcelona = (41.3851, 2.1734)

# Sin desempaquetado: repetitivo e índice-dependiente
dist_ugly = calculate_distance(madrid[0], madrid[1], barcelona[0], barcelona[1])

# Con *: expande la tupla como argumentos posicionales
dist_clean = calculate_distance(*madrid, *barcelona)


def tag(element: str, **attrs: str) -> str:
    attr_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
    return f"<{element} {attr_str}>" if attr_str else f"<{element}>"


styles = {"class": "highlight", "id": "main-title"}
# ** expande el diccionario como keyword arguments
print(tag("h1", **styles))  # → <h1 class="highlight" id="main-title">


# ── 6. Función que devuelve múltiples valores (patrón muy común) ───────
def minmax(data: list[float]) -> tuple[float, float]:
    return min(data), max(data)  # devuelve una tupla


values = [3.1, 7.4, 1.2, 9.8, 4.5]
low, high = minmax(values)       # desempaquetado directo del retorno
print(f"Rango: {low:.1f} – {high:.1f}")


# ── 7. Cuándo el desempaquetado REDUCE legibilidad ────────────────────
row = ("ES", "Madrid", 40.4168, -3.7038, 3_300_000, "capital", True, "CET")

# ❌ Esto es un problema esperando ocurrir: siete nombres sin estructura
# code, city, lat, lon, pop, role, is_capital, tz = row

# ✅ Mejor: accede por índice con nombre explícito, o usa dataclass/namedtuple
from collections import namedtuple
City = namedtuple("City", ["code", "city", "lat", "lon", "pop", "role", "is_capital", "tz"])
madrid_full = City(*row)
print(madrid_full.city, madrid_full.pop)

Qué está pasando realmente

El desempaquetado básico de lat, lon = point es directo: Python evalúa el lado derecho, comprueba que el iterable tiene exactamente tantos elementos como nombres en el lado izquierdo, y asigna en orden. Cero magia.

El desempaquetado anidado en user_id, name, (city, country) = record refleja la forma del dato. El paréntesis en el lado izquierdo no es un grupo — es una instrucción: “este elemento es a su vez un iterable, desempácalo también”. Es como decirle a Python la forma exacta que esperas. Si la estructura no coincide, el ValueError aparece con precisión quirúrgica.

El asterisco (*middle) introduce desempaquetado extendido, disponible desde Python 3. La variable prefijada con * recibe una lista (siempre lista, nunca tupla) con todos los elementos que no fueron capturados por los otros nombres. Su posición es flexible: puede ser el primero, el último, o estar en el medio. Esto hace que patrones como “el primer elemento es especial, el resto me da igual” o “me interesa el último, el resto lo descarto” sean expresivos sin rebanadas manuales.

En los bucles for, el desempaquetado ocurre en cada iteración. for product, stock in inventory.items() es exactamente equivalente a hacer product, stock = ("manzanas", 50) en cada vuelta. Esto elimina el ruido de item[0] e item[1] y hace que el bucle lea casi como prosa. Cuando tienes tuplas de tres o más campos, el nombre correcto en el for puede reemplazar un comentario explicativo completo.

El *args en llamadas a funciones invierte el sentido: en lugar de desempacar en nombres, expande una secuencia como argumentos posicionales separados. calculate_distance(*madrid, *barcelona) pasa cuatro floats a una función que espera cuatro parámetros — el intérprete hace el conteo en la llamada. El **kwargs hace lo mismo pero con diccionarios y parámetros nombrados, lo que es especialmente útil cuando construyes atributos o configuraciones dinámicamente y los pasas a una API.

El ejemplo de minmax ilustra el patrón más común en Python: una función que retorna una tupla de valores relacionados, y el llamador los desempaca inmediatamente. Esto es idiomático Python — no necesitas una clase ni un objeto de resultado cuando dos nombres son suficientes.

El bloque final es el aviso importante. Desempacar ocho campos de una fila plana crea dependencia frágil de la posición — si alguien reordena las columnas, el código falla en silencio o con un ValueError lejos del origen. Cuando una tupla tiene más de tres o cuatro campos con significado, un namedtuple o dataclass comunica la estructura y encapsula el desempaquetado en un solo sitio.

Errores que debes conocer

Error: cantidad de nombres distinta a cantidad de elementos — Python no trunca ni rellena, lanza ValueError inmediatamente.

# ❌ Wrong
x, y = (1, 2, 3)
# ValueError: too many values to unpack (expected 2)

# ✅ Right
x, y, z = (1, 2, 3)
# — o si realmente solo necesitas los dos primeros:
x, y, *_ = (1, 2, 3)

El *_ captura el sobrante y lo descarta con la convención de nombre _.


Error: asumir que la variable *rest es una tupla — siempre es una lista, lo que importa si la pasas a código que espera un tipo específico.

# ❌ Wrong
first, *rest = (1, 2, 3, 4)
result = some_function_expecting_tuple(rest)   # rest es list, no tuple

# ✅ Right
first, *rest = (1, 2, 3, 4)
result = some_function_expecting_tuple(tuple(rest))

La conversión explícita hace el contrato visible para quien lea el código después.


Error: usar ** para expandir un diccionario cuyos keys no coinciden con los parámetros de la función.

# ❌ Wrong
def greet(name: str, age: int) -> str:
    return f"Hola {name}, tienes {age} años"

data = {"name": "Ana", "city": "Madrid"}  # 'city' no es parámetro de greet
greet(**data)
# TypeError: greet() got an unexpected keyword argument 'city'

# ✅ Right
data = {"name": "Ana", "age": 30}
greet(**data)

El diccionario tiene que ser un subconjunto exacto (o la función debe aceptar **kwargs) — Python no ignora claves desconocidas.

31

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio