copy.replace: Update Immutable Objects in Python

Published on: October 1, 2026
Reading time: 5 minutes
Developer working with immutable objects and Python copy.replace

Python copy.replace creates a new version of an object while replacing only selected fields and leaving the original instance unchanged. It is useful for immutable data, configuration objects, processing results, domain models, and application states that must be updated predictably.

Instead of mutating attributes directly, you describe which values should change and receive another object. This approach works well with functional programming, testing, concurrency, and codebases where side effects must remain controlled.

What copy.replace does

copy.replace(obj, **changes) creates a new object of the same type as obj and replaces the fields passed in changes. It is not a deep copy. Unchanged fields keep the sharing and copying behavior defined by the object’s type.

Support is based on the __replace__ protocol. Compatible types can implement this method to define how the new instance is built. Important use cases include named tuples, dataclasses, and custom classes that implement the protocol.

from copy import replace
from dataclasses import dataclass

@dataclass(frozen=True)
class User:
    name: str
    email: str
    active: bool = True

original = User("Ana", "ana@example.com")
updated = replace(original, email="ana@company.com")

print(original)
print(updated)

The original remains intact. The new instance receives the updated email and preserves every other value.

Why use it instead of mutating attributes

Direct mutation is simple, but it can produce hidden side effects when the same object is shared by several parts of a program. One component may change a value that another component still expects.

With immutable replacement, each step receives a new version. That makes state transitions easier to trace, compare, test, and undo.

new_config = replace(old_config, timeout=30)

This line clearly communicates that a new configuration is being created. There is no ambiguity about whether the previous instance was modified.

Using copy.replace with dataclasses

Dataclasses are one of the most natural use cases. They define structured records with little boilerplate and can be made immutable with frozen=True.

from dataclasses import dataclass
from copy import replace

@dataclass(frozen=True)
class Product:
    name: str
    price: float
    stock: int

product = Product("Keyboard", 199.90, 15)
sale = replace(product, price=169.90)

The result is a new Product. Name and stock are preserved while price changes.

This pattern is useful in calculation pipelines. One stage can apply a discount, another can calculate tax, and another can adjust availability, each returning a new state.

Using it with namedtuple

Named tuples also represent lightweight immutable records. Replacement avoids manually rebuilding every field or remembering positional argument order.

from collections import namedtuple
from copy import replace

Point = namedtuple("Point", "x y")
p1 = Point(10, 20)
p2 = replace(p1, y=25)

A common interface reduces differences between supported record types and makes refactoring easier.

Custom classes with __replace__

A class can participate in the protocol by implementing __replace__. The method should validate field names and return a new instance.

class Account:
    def __init__(self, owner, balance, limit):
        self.owner = owner
        self.balance = balance
        self.limit = limit

    def __replace__(self, **changes):
        allowed = {"owner", "balance", "limit"}
        invalid = set(changes) - allowed
        if invalid:
            raise TypeError(f"Invalid fields: {invalid}")

        data = {
            "owner": self.owner,
            "balance": self.balance,
            "limit": self.limit,
        }
        data.update(changes)
        return type(self)(**data)

A robust implementation should reject unknown fields, preserve invariants, and keep subclasses when that behavior is appropriate.

Validation and invariants

Replacement must not allow invalid states. If an account cannot have a negative limit, validation should happen in the constructor or inside __replace__.

class Settings:
    def __init__(self, retries, timeout):
        if retries < 0:
            raise ValueError("retries cannot be negative")
        if timeout <= 0:
            raise ValueError("timeout must be positive")
        self.retries = retries
        self.timeout = timeout

When validation lives in the constructor, every replacement-created instance passes through the same rules.

Difference from copy.copy

copy.copy creates a shallow copy. It duplicates the outer object but does not provide a declarative way to replace selected fields as part of the operation.

from copy import copy, replace

shallow = copy(original)
new = replace(original, active=False)

Use copy.copy when you only need shallow duplication. Use copy.replace when you want a new version with explicit field changes.

Difference from copy.deepcopy

copy.deepcopy attempts to recursively duplicate nested objects. That can be expensive and is not always desirable. Connections, locks, file handles, and external resources should not be duplicated automatically.

copy.replace is more precise. Only named fields change, while everything else follows the object’s own semantics.

Be careful with mutable fields

An immutable outer record does not automatically make nested values immutable.

from dataclasses import dataclass
from copy import replace

@dataclass(frozen=True)
class Order:
    items: list[str]
    status: str

first = Order(["book"], "new")
second = replace(first, status="paid")
first.items.append("pen")

Both instances still share the same list. To avoid this, prefer immutable containers such as tuples or create a new collection during replacement.

second = replace(first, items=(*first.items, "pen"))

Layered configuration

A practical use case is building configuration in stages. You may start with defaults, apply environment settings, and finally add user-specific options.

base = Config(timeout=10, debug=False, retries=2)
production = replace(base, timeout=30)
local = replace(base, debug=True)

Each configuration can be passed to different components without accidental mutation of the shared base.

Events and application state

In event-driven applications, each action can transform one state into another.

def apply_payment(order, amount):
    new_paid = order.paid_total + amount
    status = "paid" if new_paid >= order.total else order.status
    return replace(order, paid_total=new_paid, status=status)

Previous states can be stored for auditing, debugging, time-travel tests, or undo features.

Simpler tests

Tests often begin with a standard fixture and change only the field relevant to a scenario.

base_user = User("Ana", "ana@example.com", True)
inactive_user = replace(base_user, active=False)

This reduces duplication, highlights the difference between scenarios, and prevents one test from contaminating another through mutation.

Version compatibility

copy.replace is a recent Python feature. Check the project’s minimum Python version before adopting it. Libraries that support older versions may need an adapter.

try:
    from copy import replace
except ImportError:
    from dataclasses import replace

This fallback helps with dataclasses, but it does not reproduce the full generic protocol for every supported type. Document the limitation and test every supported Python version.

Common mistakes

Common mistakes include assuming the operation is a deep copy, unknowingly sharing mutable lists, accepting invalid field names in __replace__, skipping validation, and using the feature on unsupported Python versions.

Another mistake is applying replacement semantics to live resources such as sockets or database transactions. Creating a “new version” of such an object may not have a safe or meaningful interpretation.

Best practices

Prefer immutable nested values when objects will be updated by replacement. Centralize invariants in the constructor. Reject unknown names. Document which fields can change. Benchmark when replacements occur in performance-sensitive loops.

Keep transformation functions small: receive a state, calculate a new value, and return a new state without changing global objects.

Continue with the Academify guides on Python copy, dataclasses, object-oriented programming, and type hints.

The official documentation for copy and dataclasses explains exact behavior and version availability.

Conclusion

copy.replace provides a clear interface for creating new object versions with targeted changes. It is especially valuable for immutable models, configuration, events, tests, and concurrent workflows. It does not replace deep copying and still requires attention to mutable nested fields, validation, and version support. With well-designed types and centralized invariants, it makes state changes more explicit, safer, and easier to test.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Code and file structure illustrating Python pathlib.Path.info
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pathlib.Path.info: Cached File Metadata

    Learn pathlib.Path.info in Python to classify files with cached metadata, scan directories efficiently, and avoid unnecessary system calls.

    Ler mais

    Tempo de leitura: 6 minutos
    01/10/2026
    Laptop with Python testing material for asyncio loop_factory
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    loop_factory: Isolate Event Loops in asyncio Tests

    Learn loop_factory in IsolatedAsyncioTestCase for isolated, predictable asyncio tests with reliable cleanup.

    Ler mais

    Tempo de leitura: 5 minutos
    30/09/2026
    Developer navigating ZIP archive files with Python zipfile.Path
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    zipfile.Path: Browse ZIP Files Without Extraction

    Learn Python zipfile.Path to navigate, read, and validate files inside ZIP archives without extracting everything.

    Ler mais

    Tempo de leitura: 5 minutos
    30/09/2026
    Programmer working with Python email headers
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    email.headerregistry: Safer Structured Email Headers

    Learn Python email.headerregistry for structured headers, addresses, groups, dates, parameters, parsing, and safer email generation.

    Ler mais

    Tempo de leitura: 5 minutos
    29/09/2026
    Computer terminal used with Python os.unlockpt pseudoterminals
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    os.unlockpt: Control Pseudoterminals in Python

    Learn Python os.unlockpt for pseudoterminals, interactive subprocesses, safe descriptor handling, portability, and cleanup.

    Ler mais

    Tempo de leitura: 6 minutos
    29/09/2026
    Python code for threaded queues and queue.ShutDown lifecycle management
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    queue.ShutDown: Stop Queues and Workers Safely

    Learn Python queue.ShutDown to close threaded queues, release workers, reject new jobs, and avoid deadlocks.

    Ler mais

    Tempo de leitura: 5 minutos
    28/09/2026