copy.replace en Python permite crear una nueva versión de un objeto sustituyendo solo algunos campos, sin modificar la instancia original. Es útil para datos inmutables, configuraciones, resultados de procesamiento, modelos de dominio y estados de aplicaciones que deben actualizarse de forma predecible.
En lugar de cambiar atributos directamente, indicas qué valores deben cambiar y recibes otro objeto. Este enfoque combina bien con programación funcional, pruebas, concurrencia y sistemas donde los efectos secundarios deben mantenerse bajo control.
Qué hace copy.replace
copy.replace(obj, **changes) crea un nuevo objeto del mismo tipo que obj y reemplaza los campos enviados en changes. No es una copia profunda. Los campos no modificados mantienen las reglas de compartición y copia definidas por el tipo.
La compatibilidad se basa en el protocolo __replace__. Los tipos compatibles pueden implementar este método para indicar cómo debe construirse la nueva instancia. Entre los casos principales están named tuples, dataclasses y clases personalizadas.
from copy import replace
from dataclasses import dataclass
@dataclass(frozen=True)
class Usuario:
nombre: str
email: str
activo: bool = True
original = Usuario("Ana", "ana@example.com")
actualizado = replace(original, email="ana@empresa.com")
print(original)
print(actualizado)
El objeto original permanece intacto. La nueva instancia recibe el email actualizado y conserva los demás valores.
Por qué usarlo en lugar de mutar atributos
La mutación directa es sencilla, pero puede provocar efectos secundarios ocultos cuando el mismo objeto es compartido por varias partes del programa. Un componente puede cambiar un valor que otro todavía esperaba encontrar.
Con reemplazo inmutable, cada etapa recibe una nueva versión. Esto facilita rastrear cambios, comparar estados, probar funciones y revertir operaciones.
config_nueva = replace(config_anterior, timeout=30)
La línea deja claro que se crea una nueva configuración. No existe ambigüedad sobre la modificación de la instancia anterior.
copy.replace con dataclasses
Las dataclasses son uno de los usos más naturales. Permiten definir registros estructurados con poco código y pueden hacerse inmutables con frozen=True.
from dataclasses import dataclass
from copy import replace
@dataclass(frozen=True)
class Producto:
nombre: str
precio: float
stock: int
producto = Producto("Teclado", 199.90, 15)
oferta = replace(producto, precio=169.90)
El resultado es una nueva instancia de Producto. El nombre y el stock se conservan, mientras el precio cambia.
Este patrón es útil en pipelines de cálculo. Una etapa puede aplicar un descuento, otra calcular impuestos y otra ajustar disponibilidad, siempre devolviendo estados nuevos.
Uso con namedtuple
Las named tuples representan registros ligeros e inmutables. El reemplazo evita reconstruir manualmente todos los campos o recordar el orden posicional.
from collections import namedtuple
from copy import replace
Punto = namedtuple("Punto", "x y")
p1 = Punto(10, 20)
p2 = replace(p1, y=25)
Una interfaz común reduce diferencias entre estructuras compatibles y facilita refactorizaciones.
Clases personalizadas con __replace__
Una clase puede participar en el protocolo implementando __replace__. El método debe validar los nombres recibidos y devolver una nueva instancia.
class Cuenta:
def __init__(self, titular, saldo, limite):
self.titular = titular
self.saldo = saldo
self.limite = limite
def __replace__(self, **changes):
permitidos = {"titular", "saldo", "limite"}
invalidos = set(changes) - permitidos
if invalidos:
raise TypeError(f"Campos inválidos: {invalidos}")
datos = {
"titular": self.titular,
"saldo": self.saldo,
"limite": self.limite,
}
datos.update(changes)
return type(self)(**datos)
Una implementación robusta debe rechazar campos desconocidos, conservar invariantes y mantener subclases cuando sea apropiado.
Validación e invariantes
El reemplazo no debe permitir estados inválidos. Si una cuenta no puede tener un límite negativo, la validación debe realizarse en el constructor o en __replace__.
class Configuracion:
def __init__(self, reintentos, timeout):
if reintentos < 0:
raise ValueError("reintentos no puede ser negativo")
if timeout <= 0:
raise ValueError("timeout debe ser positivo")
self.reintentos = reintentos
self.timeout = timeout
Al centralizar la validación en el constructor, toda instancia creada por sustitución pasa por las mismas reglas.
Diferencia frente a copy.copy
copy.copy crea una copia superficial. Duplica la capa externa, pero no ofrece una forma declarativa de cambiar campos durante la operación.
from copy import copy, replace
copia = copy(original)
nuevo = replace(original, activo=False)
Usa copy.copy cuando solo necesites una duplicación superficial. Usa copy.replace cuando quieras una nueva versión con cambios explícitos.
Diferencia frente a copy.deepcopy
copy.deepcopy intenta duplicar recursivamente objetos internos. Puede ser costoso y no siempre es deseable. Conexiones, locks, archivos y recursos externos no deberían copiarse automáticamente.
copy.replace es más preciso: solo cambian los campos indicados y el resto sigue la semántica del tipo.
Cuidado con campos mutables
Que el registro externo sea inmutable no convierte automáticamente en inmutables sus valores internos.
from dataclasses import dataclass
from copy import replace
@dataclass(frozen=True)
class Pedido:
items: list[str]
estado: str
primero = Pedido(["libro"], "nuevo")
segundo = replace(primero, estado="pagado")
primero.items.append("bolígrafo")
Las dos instancias comparten la misma lista. Para evitarlo, prefiere contenedores inmutables, como tuplas, o crea una nueva colección durante el reemplazo.
segundo = replace(primero, items=(*primero.items, "bolígrafo"))
Configuraciones por capas
Un caso práctico consiste en construir configuraciones por etapas. Puedes empezar con valores predeterminados, aplicar ajustes del entorno y finalmente opciones del usuario.
base = Config(timeout=10, debug=False, retries=2)
produccion = replace(base, timeout=30)
local = replace(base, debug=True)
Cada configuración puede enviarse a componentes distintos sin mutar accidentalmente la base compartida.
Eventos y estado de aplicaciones
En aplicaciones orientadas a eventos, cada acción puede transformar un estado en otro.
def aplicar_pago(pedido, cantidad):
nuevo_pagado = pedido.total_pagado + cantidad
estado = "pagado" if nuevo_pagado >= pedido.total else pedido.estado
return replace(pedido, total_pagado=nuevo_pagado, estado=estado)
Los estados anteriores pueden almacenarse para auditoría, depuración, pruebas temporales o funciones de deshacer.
Pruebas más simples
En pruebas es común partir de un objeto estándar y modificar únicamente el campo relevante para cada escenario.
usuario_base = Usuario("Ana", "ana@example.com", True)
usuario_inactivo = replace(usuario_base, activo=False)
Esto reduce duplicación, destaca la diferencia entre escenarios y evita que una prueba contamine otra mediante mutación compartida.
Compatibilidad entre versiones
copy.replace es una característica reciente. Comprueba la versión mínima de Python del proyecto antes de adoptarla. Las bibliotecas compatibles con versiones anteriores pueden necesitar un adaptador.
try:
from copy import replace
except ImportError:
from dataclasses import replace
Este fallback ayuda con dataclasses, pero no reproduce todo el protocolo genérico. Documenta la limitación y prueba todas las versiones soportadas.
Errores comunes
Los errores más frecuentes son creer que la operación realiza una copia profunda, compartir listas mutables sin notarlo, aceptar campos inválidos en __replace__, omitir validaciones y usar la función en versiones no compatibles.
Otro error es aplicar esta semántica a recursos vivos, como sockets o transacciones. Crear una “nueva versión” de ellos puede no tener un significado seguro.
Buenas prácticas
Prefiere valores internos inmutables cuando los objetos se actualizarán mediante reemplazo. Centraliza invariantes en el constructor. Rechaza nombres desconocidos. Documenta qué campos pueden cambiar. Mide el rendimiento si realizas muchas sustituciones en bucles críticos.
Mantén pequeñas las funciones de transformación: reciben un estado, calculan nuevos valores y devuelven otro estado sin alterar objetos globales.
Recursos relacionados
Continúa con los artículos de Academify sobre copy en Python, dataclasses, programación orientada a objetos y type hints.
La documentación oficial de copy y dataclasses explica el comportamiento exacto y la disponibilidad por versión.
Conclusión
copy.replace proporciona una interfaz clara para crear nuevas versiones de objetos con cambios puntuales. Es especialmente útil en modelos inmutables, configuraciones, eventos, pruebas y flujos concurrentes. No sustituye la copia profunda y exige atención a campos mutables, validación y compatibilidad. Con tipos bien diseñados e invariantes centralizadas, hace que los cambios de estado sean más explícitos, seguros y fáciles de probar.







