copy.replace: actualiza objetos inmutables en Python

Publicado el: 01/10/2026
Tempo de leitura: 5 minutos
Programador trabajando con objetos inmutables y copy.replace en Python

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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Estructura de archivos y código para pathlib.Path.info en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    pathlib.Path.info: caché de metadatos de archivos

    Aprende pathlib.Path.info en Python para clasificar archivos con metadatos en caché y optimizar recorridos de directorios.

    Ler mais

    Tempo de leitura: 5 minutos
    01/10/2026
    Portátil con material de pruebas en Python para loop_factory y asyncio
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    loop_factory: aísla event loops en pruebas asyncio

    Aprende loop_factory en IsolatedAsyncioTestCase para pruebas asyncio aisladas, predecibles y con limpieza segura.

    Ler mais

    Tempo de leitura: 5 minutos
    30/09/2026
    Desarrolladora navegando archivos ZIP con zipfile.Path en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    zipfile.Path: navega ZIPs sin extraer archivos

    Aprende zipfile.Path en Python para navegar, leer y validar archivos dentro de ZIPs sin extraer todo.

    Ler mais

    Tempo de leitura: 4 minutos
    30/09/2026
    Programador trabajando con encabezados de correo en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    email.headerregistry: headers de correo seguros

    Aprende email.headerregistry en Python para encabezados, direcciones, grupos, fechas, parámetros y análisis seguro de correos.

    Ler mais

    Tempo de leitura: 5 minutos
    29/09/2026
    Terminal de computadora usado con pseudoterminales os.unlockpt en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    os.unlockpt: controla pseudoterminales en Python

    Aprende os.unlockpt en Python para crear pseudoterminales, controlar subprocesos interactivos y gestionar descriptores con seguridad.

    Ler mais

    Tempo de leitura: 7 minutos
    29/09/2026
    Código Python para colas con hilos y gestión de queue.ShutDown
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    queue.ShutDown: cierra colas y workers con seguridad

    Aprende queue.ShutDown en Python para cerrar colas con hilos, liberar workers y evitar bloqueos.

    Ler mais

    Tempo de leitura: 6 minutos
    28/09/2026