Cuando escribes x = 5, Python crea un objeto entero 5 y hace que el nombre x apunte a él. Pero Python ofrece varias formas de asignar que van mucho más allá de eso, y entender cómo funcionan por dentro te evita sorpresas desagradables en producción.
¿Qué pasa realmente cuando asignas?
En Python, las variables no son cajitas que contienen valores: son nombres que apuntan a objetos en memoria. Esta distinción es fundamental para todo lo que viene.
Asignación encadenada
Cuando escribes a = b = c = 0, no se crean tres objetos. Python evalúa 0 una sola vez, crea un objeto en memoria, y luego hace que los tres nombres apunten al mismo objeto. Puedes verificarlo con id(), que devuelve la dirección de memoria del objeto.
Para enteros pequeños esto es perfectamente inofensivo, porque los enteros son inmutables: no puedes modificar el objeto 0, solo puedes hacer que a apunte a otro objeto. El peligro aparece con objetos mutables como listas. Si haces a = b = c = [], los tres nombres apuntan a la misma lista, y modificar una modifica “todas”.
Asignación múltiple con tuple unpacking
La sintaxis a, b = 1, 2 se llama desempaquetado de tuplas (tuple unpacking). El lado derecho forma una tupla (1, 2), y Python asigna cada elemento al nombre correspondiente de la izquierda. Si la cantidad no coincide, obtienes un ValueError inmediatamente.
El intercambio sin variable temporal
a, b = b, a funciona correctamente porque Python evalúa todo el lado derecho primero, construye la tupla (b, a) con los valores actuales, y solo entonces realiza las asignaciones de izquierda a derecha. No necesitas ninguna variable temporal, al contrario que en C o Java.
Asignación aumentada y __iadd__
El operador += no es simplemente azúcar sintáctico de x = x + 1. Primero intenta llamar al método __iadd__ del objeto (modificación in-place). Si el objeto no lo implementa, cae de vuelta a __add__ y reasigna el nombre. Las listas implementan __iadd__, así que mi_lista += [4] modifica la lista original. Los enteros y strings no lo implementan, así que siempre crean un objeto nuevo.
Desempaquetado extendido con asterisco
El * en la izquierda de una asignación captura “el resto” como una lista. primero, *resto = [1, 2, 3, 4] asigna 1 a primero y [2, 3, 4] a resto. La variable con asterisco puede ir en cualquier posición: al inicio, al medio o al final.
# Todos los imports necesarios para los ejemplos
# (ninguno en este caso: todo es Python puro)
# ── 1. Asignación encadenada ──────────────────────────────────────────
a = b = c = 0
print(id(a) == id(b) == id(c)) # True: el mismo objeto entero
# El problema con mutables:
x = y = []
x.append(42)
print(y) # [42] — misma lista, no una copia
# Corrección: crear objetos independientes
x, y = [], []
x.append(42)
print(y) # [] — listas distintas
# ── 2. Tuple unpacking ────────────────────────────────────────────────
nombre, edad, activo = "Ana", 30, True
print(nombre, edad, activo) # Ana 30 True
# Funciona con cualquier iterable en el lado derecho
coordenadas = (40.4168, -3.7038)
latitud, longitud = coordenadas
print(f"Madrid: {latitud}°N, {longitud}°E")
# ── 3. Intercambio sin variable temporal ──────────────────────────────
p, q = 10, 20
p, q = q, p # Python evalúa (q, p) → (20, 10) antes de asignar
print(p, q) # 20 10
# ── 4. Asignación aumentada: diferencia entre mutables e inmutables ───
# Con entero (inmutable): += crea un nuevo objeto
numero = 100
original_id = id(numero)
numero += 1
print(id(numero) == original_id) # False: objeto nuevo
# Con lista (mutable): += modifica in-place via __iadd__
items = [1, 2, 3]
original_id = id(items)
items += [4] # llama a items.__iadd__([4])
print(id(items) == original_id) # True: misma lista
print(items) # [1, 2, 3, 4]
# Contraste con + puro: siempre crea objeto nuevo
items2 = [1, 2, 3]
original_id2 = id(items2)
items2 = items2 + [4] # __add__, no __iadd__
print(id(items2) == original_id2) # False: objeto nuevo
# ── 5. Desempaquetado extendido con * ─────────────────────────────────
primero, *resto = [1, 2, 3, 4, 5]
print(primero) # 1
print(resto) # [2, 3, 4, 5] ← siempre es lista, aunque capture uno
*inicio, ultimo = [1, 2, 3, 4, 5]
print(inicio) # [1, 2, 3, 4]
print(ultimo) # 5
# * en el medio
cabeza, *cuerpo, cola = range(6)
print(cabeza, cuerpo, cola) # 0 [1, 2, 3, 4] 5
# Si el resto está vacío, la variable * recibe lista vacía
solo, *nada = [42]
print(nada) # []
Qué significa cada decisión
La asignación encadenada con inmutables como int o str es idiomática y segura: x = y = z = 0 es perfectamente normal para inicializar contadores. El riesgo real aparece cuando alguien aplica el mismo patrón a listas o diccionarios sin pensar. La regla práctica: si el objeto tiene métodos que lo modifican, no uses asignación encadenada.
El tuple unpacking es una de las características más expresivas de Python. Cuando una función devuelve múltiples valores —técnicamente devuelve una tupla— el desempaquetado en el punto de llamada hace el código mucho más legible que indexar con [0], [1]. Fíjate en que funciona con cualquier iterable, no solo tuplas: listas, range, generadores, lo que sea.
El intercambio a, b = b, a parece magia pero no lo es. Python garantiza la semántica de “construir la tupla derecha primero” precisamente para que este patrón sea seguro. Internamente, el bytecode usa instrucciones ROT_TWO o ROT_THREE que manejan esto sin crear ni una tupla real en muchos casos, así que también es eficiente.
La diferencia entre += y = ... + importa cuando tienes múltiples referencias al mismo objeto. Si a y b apuntan a la misma lista y haces a += [1], la lista que ve b también cambia. Si haces a = a + [1], a pasa a apuntar a una lista nueva y b sigue viendo la original. No hay una opción universalmente correcta: depende de si quieres mutación compartida o no.
El asterisco en desempaquetado resuelve un problema clásico: separar la cabeza de una secuencia del resto sin saber su longitud. La variable * siempre recibe una list, aunque sea vacía. Solo puede haber un * por asignación —Python no podría resolver dos de forma determinista.
Errores que debes conocer
Error: usar asignación encadenada con listas pensando que crea objetos independientes.
# ❌ Wrong filas = [[]] * 3 # las tres sublistas son el mismo objeto filas[0].append(1) print(filas) # [[1], [1], [1]] — sorpresa # ✅ Right filas = [[] for _ in range(3)] # cada comprensión crea una lista nueva filas[0].append(1) print(filas) # [[1], [], []]
Aunque * 3 no es asignación encadenada exactamente, el mecanismo subyacente es el mismo: copiar la referencia, no el objeto.
Error: esperar que += sobre una tupla funcione igual que sobre una lista.
# ❌ Wrong (comportamiento confuso) t = (1, 2) t += (3,) # t ahora es (1, 2, 3) — PERO se creó un objeto nuevo, no se modificó el original # Si t estaba dentro de otro objeto mutable, ese objeto sigue viendo (1, 2) # ✅ Right # Si necesitas colección que crece, usa lista desde el principio t = [1, 2] t += [3] # modifica in-place
Las tuplas no implementan __iadd__, así que += crea una tupla nueva y reasigna el nombre local. Si esa tupla vivía como campo de otro objeto, ese objeto no se entera del cambio.
Error: poner más de un * en el desempaquetado.
# ❌ Wrong — SyntaxError primero, *medio, *final = [1, 2, 3, 4] # ✅ Right — un solo * por asignación primero, *resto = [1, 2, 3, 4] medio, final = resto[:-1], resto[-1]
Python no puede determinar cómo distribuir elementos entre dos capturadores de “resto” sin reglas adicionales, así que es directamente un error de sintaxis.
N° 30