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.
Related resources
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.







