copy en Python: copia superficial y profunda

Publicado el: 28/07/2026
Tempo de leitura: 7 minutos
Icono de documentos duplicados que representa copias superficiales y profundas en Python

Asignar una lista, un diccionario o una instancia a otra variable no crea automáticamente un objeto nuevo. En Python, la asignación normalmente crea otro nombre que apunta al mismo valor. Por eso, una modificación realizada desde una variable puede aparecer inesperadamente en la otra. El módulo copy en Python ofrece herramientas explícitas para copias superficiales, copias profundas y reemplazo de campos en objetos de estilo inmutable.

Esta guía explica identidad frente a igualdad, copy.copy(), copy.deepcopy(), el diccionario interno memo, la personalización mediante __copy__() y __deepcopy__(), y la función copy.replace(). El contenido complementa los artículos sobre listas en Python, slicing en Python, igualdad e identidad, diccionarios en Python y descriptors en Python.

Asignar no significa copiar

original = [1, 2, 3]
alias = original

alias.append(4)

print(original)  # [1, 2, 3, 4]
print(alias is original)  # True

Ambos nombres señalan la misma lista. append() modifica el objeto compartido. El operador is confirma identidad: las dos variables apuntan exactamente al mismo objeto.

Este comportamiento es eficiente y suele ser correcto. Solo necesitas una copia cuando el programa requiere modificaciones independientes.

Copia superficial con copy.copy()

copy.copy() crea un nuevo objeto compuesto, pero reutiliza las referencias a los objetos internos. Esto se denomina copia superficial.

import copy

original = [[1, 2], [3, 4]]
superficial = copy.copy(original)

print(superficial is original)  # False
print(superficial[0] is original[0])  # True

La lista exterior es nueva, pero las listas interiores siguen compartidas. Añadir una fila nueva a la copia no modifica el origen. Cambiar una fila existente afecta a ambas.

superficial.append([5, 6])
superficial[0].append(99)

print(original)     # [[1, 2, 99], [3, 4]]
print(superficial)  # [[1, 2, 99], [3, 4], [5, 6]]

Métodos nativos de copia

Las listas, diccionarios y conjuntos tienen métodos convenientes:

lista_nueva = lista_antigua.copy()
diccionario_nuevo = diccionario_antiguo.copy()
conjunto_nuevo = conjunto_antiguo.copy()

Un slice completo también produce una copia superficial de una lista o secuencia:

nueva = antigua[:]

Estas variantes son claras cuando el código conoce el tipo concreto. copy.copy() resulta útil en funciones genéricas e instancias personalizadas. La documentación oficial del módulo copy también indica que algunos métodos nativos y slices pueden devolver una instancia del tipo base al copiar una subclase, mientras copy.copy() normalmente conserva el tipo concreto.

Copia profunda con copy.deepcopy()

copy.deepcopy() recorre recursivamente un objeto compuesto y copia sus componentes. Así evita compartir accidentalmente valores mutables anidados.

import copy

original = {
    "usuario": "Ana",
    "permisos": ["leer", "editar"],
    "preferencias": {"tema": "oscuro"},
}

profunda = copy.deepcopy(original)
profunda["permisos"].append("publicar")
profunda["preferencias"]["tema"] = "claro"

print(original["permisos"])
print(original["preferencias"])

La lista y el diccionario internos son independientes, por lo que cambiar la copia no altera el original.

Deepcopy no duplica todo

Una copia profunda no garantiza una identidad nueva para cada valor. Los objetos inmutables, funciones y clases pueden reutilizarse. Los módulos, archivos abiertos, sockets, frames de ejecución, ventanas y recursos similares no se copian de forma genérica.

La copia recursiva también puede duplicar demasiado. Conexiones, locks, servicios compartidos, registros y cachés globales normalmente deben seguir siendo únicos. Usa deepcopy() solo cuando la independencia recursiva forme parte del contrato del objeto.

Referencias recursivas

Un objeto puede apuntar a sí mismo:

elementos = []
elementos.append(elementos)

Un copiador recursivo ingenuo entraría en un bucle infinito. deepcopy() evita ese problema mediante un diccionario interno llamado memo, que registra los objetos ya procesados durante la operación.

import copy

elementos = []
elementos.append(elementos)
copia = copy.deepcopy(elementos)

print(copia is copia[0])     # True
print(copia is elementos)    # False

La autorreferencia se conserva sin reutilizar la lista original.

Por qué memo es importante

El diccionario memo también conserva el uso compartido interno. Si dos rutas del objeto original apuntan al mismo componente, el grafo copiado normalmente hará que ambas rutas apunten a una única copia de ese componente.

import copy

configuracion = {"limite": 10}
original = [configuracion, configuracion]
copia = copy.deepcopy(original)

print(copia[0] is copia[1])       # True
print(copia[0] is configuracion)  # False

Esta propiedad evita duplicaciones inconsistentes y conserva relaciones de identidad importantes.

Una copia profunda puede copiar demasiado

Considera un objeto de dominio que contiene datos y un servicio externo compartido:

class Pedido:
    def __init__(self, elementos, pasarela):
        self.elementos = elementos
        self.pasarela = pasarela

Los elementos pueden necesitar independencia, mientras la pasarela de pago debe seguir compartida. Una copia profunda automática no siempre expresa esa regla. La solución es personalizar el comportamiento.

Personalizar __copy__()

Una clase puede definir __copy__() para controlar su copia superficial:

import copy

class Documento:
    def __init__(self, titulo, metadatos):
        self.titulo = titulo
        self.metadatos = metadatos

    def __copy__(self):
        return type(self)(self.titulo, self.metadatos)

documento = Documento("Informe", {"version": 1})
nuevo = copy.copy(documento)

print(nuevo is documento)
print(nuevo.metadatos is documento.metadatos)

La instancia es nueva, pero los metadatos permanecen compartidos, que es la semántica superficial esperada.

Personalizar __deepcopy__()

__deepcopy__(self, memo) recibe el diccionario de memoización. Debes tratarlo como una estructura opaca y pasarlo a las llamadas recursivas de copy.deepcopy().

import copy

class Pedido:
    def __init__(self, elementos, pasarela):
        self.elementos = elementos
        self.pasarela = pasarela

    def __deepcopy__(self, memo):
        nuevo = type(self)(
            copy.deepcopy(self.elementos, memo),
            self.pasarela,
        )
        memo[id(self)] = nuevo
        return nuevo

La colección de elementos se vuelve independiente, mientras el servicio continúa compartido. Registrar la nueva instancia en memo es especialmente importante cuando existen ciclos o referencias repetidas.

Actualizaciones inmutables con copy.replace()

Agregada en Python 3.13, copy.replace() crea un objeto nuevo del mismo tipo reemplazando solamente campos seleccionados. Es una opción práctica para registros de estilo inmutable.

from copy import replace
from dataclasses import dataclass

@dataclass(frozen=True)
class Producto:
    nombre: str
    precio: float
    stock: int

original = Producto("Teclado", 120.0, 15)
promocion = replace(original, precio=99.9)

print(original)
print(promocion)

El objeto de origen queda intacto. La función soporta dataclasses, named tuples y clases que implementan __replace__().

copy.replace() no es deepcopy()

replace() sustituye campos concretos y reutiliza los valores restantes. Un campo mutable que no se reemplaza continúa compartido.

from copy import replace
from dataclasses import dataclass

@dataclass(frozen=True)
class Plan:
    nombre: str
    pasos: list[str]

base = Plan("Lanzamiento", ["prueba", "produccion"])
nuevo = replace(base, nombre="Lanzamiento v2")

print(nuevo.pasos is base.pasos)  # True

Utiliza replace() para actualizaciones funcionales de registros, no como sinónimo de independencia recursiva.

Implementar __replace__()

Las clases personalizadas pueden adoptar el protocolo:

class Coordenada:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __replace__(self, **changes):
        permitidos = {"x", "y"}
        desconocidos = set(changes) - permitidos
        if desconocidos:
            raise TypeError(f"Campos inválidos: {desconocidos}")
        return type(self)(
            changes.get("x", self.x),
            changes.get("y", self.y),
        )

Después, copy.replace(coordenada, x=10) crea una nueva instancia con una actualización controlada.

Diccionarios anidados y trampas de copia superficial

predeterminado = {
    "api": {"timeout": 10},
    "logs": {"nivel": "INFO"},
}

produccion = predeterminado.copy()
produccion["api"]["timeout"] = 30

print(predeterminado["api"]["timeout"])  # 30

El diccionario exterior se copia, pero los diccionarios internos siguen compartidos. Los árboles de configuración independientes requieren reconstrucción explícita, merge controlado o una copia profunda segura.

Copiar listas de objetos

Copiar una lista no copia las instancias almacenadas:

usuarios_copia = usuarios.copy()

print(usuarios_copia is usuarios)       # False
print(usuarios_copia[0] is usuarios[0]) # True

Esto es útil cuando la nueva lista representa otro orden o una vista filtrada de los mismos objetos. Si cada usuario debe ser independiente, utiliza un método de clonación de dominio o una copia profunda cuidadosamente diseñada.

Cuándo reconstruir de forma explícita

Los modelos de dominio suelen beneficiarse de construir el nuevo valor de forma explícita. El código muestra qué campos se copian, recalculan o comparten intencionalmente.

nuevo_pedido = Pedido(
    elementos=[elemento.clonar() for elemento in pedido.elementos],
    pasarela=pedido.pasarela,
)

Esta estrategia es más extensa, pero puede comunicar la propiedad de los objetos mejor que una copia recursiva genérica.

Rendimiento y memoria

Una copia superficial suele costar tiempo proporcional al número de referencias del contenedor exterior. Una copia profunda recorre el grafo alcanzable y puede consumir mucha más CPU y memoria.

Evita copiar estructuras grandes repetidamente sin medir. Los valores inmutables, el uso compartido estructural, el reemplazo de campos, los generadores y el almacenamiento persistente pueden ser mejores alternativas. El tutorial oficial de estructuras de datos aporta contexto sobre listas, diccionarios, conjuntos y secuencias.

Probar la independencia correcta

No pruebes solamente igualdad. Verifica identidad y mutaciones posteriores:

import copy

original = {"elementos": [[1], [2]]}
copia = copy.deepcopy(original)

assert copia == original
assert copia is not original
assert copia["elementos"] is not original["elementos"]
assert copia["elementos"][0] is not original["elementos"][0]

copia["elementos"][0].append(9)
assert original["elementos"][0] == [1]

Las clases personalizadas también deben probarse con ciclos, referencias repetidas, recursos compartidos y subclases.

Errores frecuentes

  • Suponer que b = a crea un objeto nuevo.
  • Usar list.copy() esperando duplicar elementos anidados.
  • Aplicar deepcopy() sin criterio a sockets, locks, conexiones o servicios.
  • Destruir relaciones de identidad significativas dentro del grafo.
  • Implementar __deepcopy__() sin reenviar memo.
  • Tratar copy.replace() como una copia profunda.
  • Duplicar estructuras grandes sin medir el coste.
  • Copiar cuando una reconstrucción explícita sería más clara.

Buenas prácticas

  • Define primero el nivel de independencia requerido.
  • Usa copia superficial cuando solo debe cambiar el contenedor exterior.
  • Usa copia profunda cuando todo el estado mutable alcanzable debe ser independiente.
  • Personaliza las clases que contienen recursos compartidos.
  • Usa copy.replace() para actualizaciones funcionales de registros compatibles.
  • Documenta qué componentes siguen compartidos.
  • Prueba igualdad, identidad y mutaciones posteriores.
  • Mide tiempo y memoria en grafos grandes.

Conclusión

El módulo copy en Python resuelve una distinción fundamental: crear otro nombre, copiar solamente el objeto exterior y duplicar recursivamente un grafo son operaciones diferentes. copy.copy() crea una copia superficial. copy.deepcopy() utiliza memoización para tratar ciclos y conservar el uso compartido interno. copy.replace() crea versiones actualizadas de registros compatibles sustituyendo campos concretos.

La elección correcta depende de la propiedad de los objetos. Las copias superficiales son eficientes y comparten componentes intencionalmente. Las copias profundas aportan independencia, pero pueden duplicar demasiado. Los protocolos personalizados combinan ambas estrategias cuando un objeto contiene responsabilidades mixtas. Hacer explícita esta decisión evita mutaciones inesperadas, consumo innecesario de memoria y duplicación incorrecta de recursos.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Monitor con búsqueda binaria y listas ordenadas en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    bisect en Python: listas siempre ordenadas

    Aprende bisect en Python para mantener listas ordenadas, localizar rangos, encontrar vecinos e insertar valores con búsqueda binaria.

    Ler mais

    Tempo de leitura: 9 minutos
    27/07/2026
    Desarrollador implementando una cola de prioridad con heapq en Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    heapq en Python: colas de prioridad

    Aprende heapq en Python para crear colas de prioridad, encontrar valores mínimos y procesar tareas con heaps eficientes.

    Ler mais

    Tempo de leitura: 7 minutos
    26/07/2026
    Compactando arquivos ZIP automaticamente com Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    Cómo crear archivos ZIP con Python y zipfile

    Crea archivos ZIP con Python y zipfile: carpetas, filtros, arcname, compresión, contenido en memoria, verificación, hashes y backups seguros.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Como evitar KeyError usando defaultdict em Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    defaultdict en Python: evita KeyError y simplifica diccionarios

    Aprende defaultdict en Python para evitar KeyError, contar, agrupar, crear estructuras anidadas y compararlo con get, setdefault y Counter.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Redimensionamento de imagens com Pillow em Python
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    Cómo redimensionar imágenes con Pillow en Python

    Redimensiona imágenes con Pillow en Python conservando proporciones, creando miniaturas, corrigiendo EXIF, procesando carpetas y exportando WebP.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Copiando e movendo arquivos com Python usando shutil
    Bibliotecas y Módulos
    Foto de perfil de Leandro Hirt da Academify

    Cómo copiar y mover archivos en Python con shutil

    Aprende shutil en Python para copiar, mover, eliminar y comprimir archivos y carpetas con validaciones, hashes, logs y modo simulación.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026