Cuando escribes b = a en Python, no estás duplicando ningún dato. Estás creando una segunda etiqueta que apunta al mismo objeto en memoria. Eso es el aliasing: dos nombres distintos que referencian exactamente el mismo objeto.
Python trata las variables como referencias, no como cajones donde se guarda un valor. Cuando asignas a = [1, 2, 3], Python crea una lista en memoria y a guarda la dirección de esa lista. Al hacer b = a, Python copia la dirección, no el contenido. Ahora a y b apuntan al mismo objeto.
Este diseño no es accidental. Es una decisión de rendimiento: copiar objetos grandes en cada asignación sería prohibitivamente caro. Python asume que si quieres una copia, la pedirás explícitamente.
El problema llega cuando modificas el objeto a través de uno de los nombres y el cambio aparece también en el otro. Si no esperabas eso, tienes un bug silencioso que puede ser muy difícil de rastrear.
Para detectar aliasing, usas el operador is. a == b comprueba si los valores son iguales; a is b comprueba si son literalmente el mismo objeto en memoria. Cuando hay aliasing, a is b devuelve True.
Cuando necesitas independencia real entre dos listas o diccionarios, tienes dos opciones: copia superficial o copia profunda. La copia superficial (list.copy(), dict.copy(), o el slice [:]) crea un nuevo contenedor pero reutiliza las referencias a los objetos internos. Si la lista contiene otras listas, esas listas internas siguen siendo compartidas. La copia profunda (copy.deepcopy()) recorre toda la estructura de forma recursiva y duplica cada objeto que encuentra, sin dejar ninguna referencia compartida.
import copy
# --- Aliasing básico ---
original = [1, 2, 3]
alias = original # alias ES original, no una copia
alias.append(4)
print(original) # [1, 2, 3, 4] ← se modificó también
print(alias is original) # True
# --- Copia superficial ---
base = [10, 20, [30, 40]]
shallow = base.copy() # nuevo contenedor, mismos objetos internos
shallow.append(99)
print(base) # [10, 20, [30, 40]] ← OK, el contenedor es distinto
shallow[2].append(50) # modifica la lista interna compartida
print(base) # [10, 20, [30, 40, 50]] ← base también cambió
print(shallow is base) # False ← contenedores distintos
print(shallow[2] is base[2]) # True ← lista interna compartida
# --- Copia profunda ---
source = [1, [2, 3], [4, [5, 6]]]
deep = copy.deepcopy(source) # duplica todo de forma recursiva
deep[1].append(99)
deep[2][1].append(99)
print(source) # [1, [2, 3], [4, [5, 6]]] ← intacto
print(deep[1] is source[1]) # False ← objetos internos también son distintos
# --- Aliasing intencional: patrón útil ---
config = {"debug": False, "retries": 3}
def apply_defaults(cfg):
cfg["timeout"] = 30 # modifica el diccionario original intencionalmente
return cfg
result = apply_defaults(config)
print(config) # {"debug": False, "retries": 3, "timeout": 30}
print(result is config) # True ← la función devuelve el mismo objeto
Qué está pasando en cada caso
La primera sección demuestra el aliasing en su forma más directa. alias y original son el mismo objeto, por eso append en uno se ve en el otro. El is lo confirma sin ambigüedad.
La copia superficial rompe el aliasing en el nivel exterior: shallow y base son contenedores distintos, así que shallow.append(99) no toca base. Pero los elementos que ya estaban dentro, como la lista [30, 40], no se duplicaron. Ambas listas guardan una referencia al mismo objeto [30, 40], y cuando lo modificas a través de shallow[2], base[2] refleja el cambio. El slice [:] se comporta exactamente igual que .copy().
La copia profunda con copy.deepcopy() es la única que garantiza independencia total. Recorre la estructura entera y crea objetos nuevos para cada nivel. Después de deepcopy, no hay ninguna referencia compartida entre source y deep. El precio es tiempo y memoria proporcionales al tamaño de la estructura.
El último bloque muestra aliasing como herramienta, no como bug. Cuando una función recibe un diccionario y lo modifica directamente, está usando el aliasing de forma deliberada: no necesita devolver una copia porque trabaja sobre el objeto real del caller. Es un patrón legítimo siempre que esté documentado y sea la intención explícita.
Errores que debes conocer
Error: usar [:] para copiar una lista de listas pensando que es una copia completa.
# ❌ Wrong matrix = [[1, 2], [3, 4]] copy_matrix = matrix[:] copy_matrix[0].append(99) print(matrix) # [[1, 2, 99], [3, 4]] ← matrix también cambió # ✅ Right import copy matrix = [[1, 2], [3, 4]] copy_matrix = copy.deepcopy(matrix) copy_matrix[0].append(99) print(matrix) # [[1, 2], [3, 4]] ← intacto
El slice crea un nuevo contenedor, pero las listas internas siguen siendo las mismas objetos; deepcopy es necesario cuando la estructura tiene más de un nivel.
Error: comparar con == para detectar aliasing cuando lo que necesitas saber es si es el mismo objeto.
# ❌ Wrong a = [1, 2, 3] b = [1, 2, 3] # lista distinta, mismo contenido print(a == b) # True ← no detecta que son objetos distintos # ✅ Right print(a is b) # False ← confirma que son objetos diferentes
== delega en __eq__ y compara valores; is compara identidad de objeto (la dirección en memoria), que es lo que importa cuando buscas aliasing.
N° 33