Si alguna vez intentaste copiar una lista o diccionario en Python y terminaste modificando ambos sin querer, esta guía es para ti. El comportamiento de copia en Python es una de las fuentes más comunes de errores sutiles, especialmente para quienes están comenzando. Entender la diferencia entre copia superficial (shallow copy) y copia profunda (deep copy) es esencial para escribir código predecible y libre de efectos secundarios.

En esta guía completa, aprenderás desde el concepto de asignación vs copia hasta técnicas avanzadas con el módulo copy de la biblioteca estándar. Exploraremos ejemplos prácticos con listas, diccionarios, objetos anidados y clases personalizadas, además de discutir rendimiento y buenas prácticas.

Asignación No Es Copia

Antes de sumergirnos en los tipos de copia, es fundamental entender que asignar una variable a otra no crea una copia. En Python, las variables son etiquetas (referencias) que apuntan a objetos en memoria. Cuando haces b = a, solo estás creando una nueva etiqueta para el mismo objeto. La documentación oficial de Python sobre asignación explica que el operador = vincula el nombre al objeto sin duplicarlo.

# Asignación común — no es copia
lista_original = [1, 2, [3, 4]]
lista_referencia = lista_original

lista_referencia.append(5) print(lista_original) # [1, 2, [3, 4], 5] — ¡la original se modificó!

Esto ocurre porque ambas variables apuntan al mismo objeto en memoria. Cualquier modificación a través de una referencia afecta a la otra. Este concepto fundamental es lo que motiva la necesidad de métodos de copia explícitos.

¿Qué es una Shallow Copy (Copia Superficial)?

Una shallow copy (copia superficial) crea un objeto nuevo, pero no duplica los objetos internos. En su lugar, copia las referencias a los elementos contenidos en el objeto original. Esto significa que el objeto de nivel superior es nuevo, pero los objetos anidados (como listas dentro de listas) siguen siendo compartidos entre la copia y el original.

Python ofrece varias formas de crear shallow copies. La más común es usando el módulo copy:

import copy

lista_original = [1, 2, [3, 4]] copia_superficial = copy.copy(lista_original)

copia_superficial.append(5) # solo afecta a la copia copia_superficial[2].append(5) # ¡afecta a AMBAS! El objeto interno se comparte

print(lista_original) # [1, 2, [3, 4, 5]] print(copia_superficial) # [1, 2, [3, 4, 5], 5]

Observa que append(5) en el nivel superior solo modificó la copia, pero append(5) en la lista anidada modificó ambas. Esto se debe a que la lista interna [3, 4] no se duplicó — solo se copió la referencia hacia ella. La documentación oficial del módulo copy describe este comportamiento en detalle.

Métodos de Shallow Copy

Además de copy.copy(), existen otras formas de crear shallow copies en Python:

  • El método .copy() de listas y diccionarios: lista.copy() y dict.copy() crean shallow copies.
  • Rebanado ([:]): nueva_lista = lista_original[:] crea una shallow copy de la lista.
  • Los constructores list() y dict(): list(original) y dict(original) también crean shallow copies.
  • El módulo copy: copy.copy(objeto) funciona con cualquier tipo de objeto.

Para quienes trabajan con listas en Python, dominar estas técnicas es fundamental para evitar comportamientos inesperados. Real Python tiene un tutorial detallado sobre copia de objetos en Python que vale la pena consultar.

¿Qué es una Deep Copy (Copia Profunda)?

Una deep copy (copia profunda) crea un objeto nuevo y, recursivamente, duplica todos los objetos anidados que encuentra. El resultado es una copia completamente independiente del original, sin ninguna referencia compartida. Cualquier modificación en la copia profunda no afecta al original, y viceversa.

import copy

lista_original = [1, 2, [3, 4]] copia_profunda = copy.deepcopy(lista_original)

copia_profunda.append(5) copia_profunda[2].append(5)

print(lista_original) # [1, 2, [3, 4]] — ¡intacta! print(copia_profunda) # [1, 2, [3, 4, 5], 5]

La función copy.deepcopy() recorre todo el árbol de objetos y crea duplicados de cada nodo que encuentra. Esto garantiza un aislamiento total entre la copia y el original, pero tiene un costo computacional más alto, especialmente para estructuras grandes o con muchos niveles de anidamiento. La discusión en Stack Overflow sobre este tema es una de las más votadas de la plataforma, lo que demuestra su relevancia.

Cómo Funciona deepcopy Internamente

La función copy.deepcopy() usa un diccionario interno de "memoria" (memo) para rastrear objetos ya copiados. Esto es esencial para evitar bucles infinitos en estructuras con referencias circulares y para garantizar que los objetos compartidos no se dupliquen múltiples veces. Si estás estudiando diccionarios en Python, entender deep copy es fundamental, ya que los diccionarios con valores anidados son uno de los casos de uso más comunes.

El mecanismo se puede resumir en tres pasos:

  1. Verificación del memo: Si el objeto ya fue copiado, retorna la copia existente.
  2. Creación del nuevo objeto: Usa __deepcopy__ o __copy__ si están definidos, o crea un nuevo objeto vacío del mismo tipo.
  3. Copia recursiva: Para cada atributo o elemento, llama a deepcopy recursivamente y lo almacena en el nuevo objeto.

La PEP 8 — Guía de Estilo Python recomienda implementar métodos de copia explícitos en las clases que necesitan comportamiento personalizado.

Shallow Copy vs Deep Copy: Comparación Directa

Consolidemos las diferencias en una tabla comparativa para consulta rápida:

CaracterísticaShallow CopyDeep Copy
¿Crea nuevo objeto de nivel superior?
¿Copia objetos anidados?No (solo referencias)Sí (recursivamente)
RendimientoRápidaMás lenta
Uso de memoriaBajoAlto
¿Aislamiento total?No
¿Funciona con objetos personalizados?Sí (por defecto)Sí (por defecto)
¿Referencias circulares?No causa problemaSí (vía memo)

Copia en Diferentes Estructuras de Datos

Copia de Listas

Las listas son la estructura más común donde aparece la diferencia entre shallow y deep copy. Veamos ejemplos con diferentes niveles de anidamiento:

import copy

Lista simple (sin anidamiento)

simple = [1, 2, 3] s_superficial = copy.copy(simple) s_profunda = copy.deepcopy(simple)

Ambas funcionan igual aquí porque no hay objetos anidados

Lista anidada

anidada = [[1, 2], [3, 4]] a_superficial = copy.copy(anidada) a_profunda = copy.deepcopy(anidada)

anidada[0].append(99) print(anidada) # [[1, 2, 99], [3, 4]] print(a_superficial) # [[1, 2, 99], [3, 4]] — ¡afectada! print(a_profunda) # [[1, 2], [3, 4]] — aislada

Copia de Diccionarios

import copy

datos = {"nombre": "María", "direccion": {"ciudad": "CDMX", "cp": "01001"}}

copia_sup = copy.copy(datos) copia_prof = copy.deepcopy(datos)

copia_sup["direccion"]["ciudad"] = "Guadalajara" print(datos["direccion"]["ciudad"]) # Guadalajara — shallow copy comparte el dict interno

copia_prof["direccion"]["ciudad"] = "Monterrey" print(datos["direccion"]["ciudad"]) # Guadalajara — deep copy mantuvo el original intacto

Los diccionarios con valores anidados (como el campo "direccion" de arriba) son la situación más común donde shallow copy causa sorpresas. Siempre que tu diccionario contenga otros diccionarios, listas u objetos como valores, considera usar deep copy si necesitas aislamiento completo.

Copia de Objetos Personalizados

Para tus propias clases, el comportamiento de copia se puede personalizar implementando los métodos __copy__ y __deepcopy__:

import copy

class Direccion: def init(self, ciudad, cp): self.ciudad = ciudad self.cp = cp

class Persona: def init(self, nombre, direccion): self.nombre = nombre self.direccion = direccion

def __copy__(self):
    # Shallow copy personalizada
    return Persona(self.nombre, self.direccion)

def __deepcopy__(self, memo):
    # Deep copy personalizada
    return Persona(
        copy.deepcopy(self.nombre, memo),
        copy.deepcopy(self.direccion, memo)
    )

dir = Direccion("CDMX", "01001") p1 = Persona("María", dir) p2 = copy.deepcopy(p1)

p2.direccion.ciudad = "Monterrey" print(p1.direccion.ciudad) # CDMX — aislada gracias a deepcopy

La documentación del método __deepcopy__ explica que el parámetro memo es un diccionario que rastrea objetos ya copiados para evitar duplicación y bucles infinitos.

Cuándo Usar Cada Tipo de Copia

Elegir entre shallow copy y deep copy depende de tu caso de uso. Estas son pautas prácticas:

Prefiere Shallow Copy Cuando:

  • Los objetos contenidos son inmutables (ints, strings, tuplas sin objetos mutables).
  • Buscas eficiencia y la estructura tiene solo un nivel de profundidad.
  • Intencionalmente quieres compartir objetos internos (ej: un caché compartido).
  • Trabajas con grandes volúmenes de datos donde deep copy sería muy costosa.

Prefiere Deep Copy Cuando:

  • Hay objetos mutables anidados en múltiples niveles.
  • Necesitas aislamiento completo entre original y copia.
  • Implementas patrones como snapshot de estado o rollback.
  • No tienes control sobre quién modificará los objetos internos.

Rendimiento y Buenas Prácticas

La deep copy es significativamente más costosa que la shallow copy, tanto en tiempo de ejecución como en uso de memoria. Para objetos grandes, la diferencia puede ser de órdenes de magnitud. Si estás procesando gigabytes de datos, una deep copy puede agotar fácilmente la memoria disponible.

Buenas prácticas para trabajar con copias en Python:

  1. Documenta el comportamiento: Si tu clase implementa __copy__ o __deepcopy__, documenta qué se copia superficial o profundamente.
  2. Prefiere inmutabilidad: Usa tuplas, frozensets y tipos inmutables siempre que sea posible. Nunca necesitan copia profunda.
  3. Considera alternativas: En lugar de copiar, a veces es mejor diseñar tu código para no necesitar copias (ej: usando factory functions).
  4. Cuidado con deepcopy en objetos complejos: Objetos con conexiones de red, archivos abiertos o locks no deben copiarse con deepcopy.

La documentación oficial de deepcopy advierte que objetos como módulos, clases, funciones, tipos de máquina, archivos y generadores no se pueden copiar. El módulo pickle es una alternativa para serialización y copia en algunos escenarios.

Casos Avanzados

Referencias Circulares

Las estructuras con referencias circulares (un objeto que contiene una referencia a sí mismo) son manejadas correctamente por deepcopy gracias al parámetro memo:

import copy

class Nodo: def init(self, valor): self.valor = valor self.siguiente = None

a = Nodo(1) b = Nodo(2) a.siguiente = b b.siguiente = a # ¡referencia circular!

copia = copy.deepcopy(a) # funciona sin problemas print(copia.siguiente.siguiente.valor) # 1

Copia con Filtros

Puedes controlar qué atributos se copian implementando __deepcopy__ de forma selectiva:

import copy

class Config: def init(self): self.datos = {"clave": "valor"} self.cache = {"temp": "datos"} # no queremos copiar esto

def __deepcopy__(self, memo):
    nuevo = Config()
    nuevo.datos = copy.deepcopy(self.datos, memo)
    # cache no se copia — se recreará vacío
    return nuevo

Errores Comunes y Cómo Evitarlos

Estos son los errores más frecuentes al trabajar con copias en Python y cómo evitarlos:

Error #1: Asumir que = crea una copia

Como vimos al inicio, b = a crea una nueva referencia, no una copia. Siempre usa copy() o deepcopy() cuando necesites independencia.

Error #2: Usar copy.copy() en estructuras profundamente anidadas

Si tu estructura tiene objetos mutables en múltiples niveles, shallow copy no es suficiente. Usa deepcopy() o rediseña la solución.

Error #3: Aplicar deepcopy indiscriminadamente

Deep copy es costosa. Úsala solo cuando sea necesario. Para objetos simples o inmutables, shallow copy es suficiente y mucho más eficiente. El artículo de GeeksForGeeks sobre copy en Python muestra ejemplos prácticos de esta diferencia de rendimiento.

Error #4: Ignorar objetos no copiables

No todos los objetos se pueden copiar con deepcopy. Conexiones de base de datos, sockets, hilos y objetos del sistema operativo fallarán. Siempre verifica la documentación de la biblioteca que estés usando. La documentación oficial de estructuras de datos de Python es un excelente punto de partida para entender estos conceptos a fondo.

Conclusión

Dominar la diferencia entre copy y deep copy en Python es una habilidad esencial para cualquier desarrollador que trabaje con estructuras de datos mutables. La elección correcta entre copia superficial y profunda puede prevenir errores difíciles de encontrar y mejorar significativamente la predecibilidad de tu código.

La regla de oro es simple: usa shallow copy para objetos planos o cuando sepas que los objetos internos son inmutables; usa deep copy cuando haya objetos mutables anidados y necesites aislamiento completo. Y, sobre todo, recuerda que la asignación (=) nunca es una copia.

Sigue explorando el Universo Python con nuestras guías gratuitas sobre listas en Python y diccionarios en Python para profundizar tu conocimiento en manipulación de datos en el lenguaje.