What does `copy.replace()` do, and what must a class define to support it?
answer
- A generic functional update
- One name for four older spellings
- The class does all the real work
- Shares a module with copying, not a mechanism
- Arrived in the 3.13 release
basics
~10 scopy.replace(obj, **changes), added in Python 3.13, returns a new object of the same type with the named fields replaced. It calls type(obj).replace and raises TypeError when the class does not define that method.
solid answer
~40 s`copy.replace()` is a generic *functional update*: it produces a new object like the original but with some fields changed, which is how you "modify" an immutable value. Its whole implementation is to look up `__replace__` on the object's class and call it with the keyword changes, raising `TypeError` if the class has none. Added in Python 3.13 and present in 3.14, it generalizes patterns that already existed per type — `dataclasses.replace()`, a named tuple's `_replace()`, `datetime.date.replace()` — behind one function. The standard library types that support it include dataclasses, named tuples, `datetime.date`, `datetime.time`, `datetime.datetime`, `inspect.Signature`, `inspect.Parameter` and `types.SimpleNamespace`. It is not a copy hook: it never consults `__copy__` or `__deepcopy__`, and it duplicates nothing recursively.
code
python · 16 linesimport copy
from dataclasses import dataclass
@dataclass(frozen=True)
class Window:
start: int
hits: int
w = Window(0, 83)
print(copy.replace(w, hits=90), w)
try:
copy.replace(w, misses=1)
except TypeError as exc:
print("TypeError:", exc)go deeper
Know that copy.replace(obj, **changes) returns a new object with some fields changed and leaves the original untouched — it is how you get a modified version of an immutable value such as a frozen dataclass.
Explain the mechanism exactly: it calls type(obj).__replace__ and raises TypeError when there is none, it copies nothing recursively, and it arrived in Python 3.13 alongside __replace__ on dataclasses, named tuples and the datetime types.
Judge where it belongs: define __replace__ on value objects and immutable configuration records, not on stateful service objects, and remember that a dataclass's version re-runs constructor validation while copy.copy() does not.
Own the version constraint and the API-design angle — a codebase supporting 3.12 cannot rely on it, and offering __replace__ on a public type is a commitment that its fields are constructor arguments and will stay that way.
## The problem it solves Immutable objects are excellent until you need one that is almost the same. A frozen dataclass holding a time window cannot be mutated, so "the same window with a different hit count" means constructing a new one and retyping every unchanged field — verbose, and a magnet for the bug where a field added later is forgotten at one call site. Every corner of the standard library had grown its own answer to this: `dataclasses.replace()` for dataclasses, `_replace()` for named tuples, a `replace()` method on `datetime.date`, another on `inspect.Signature`. Same idea, four spellings, none of them usable from generic code. **Python 3.13 added `copy.replace(obj, **changes)`** and the `__replace__` protocol behind it, giving that operation one name across every type that opts in. It is present unchanged in 3.14. ## The mechanism, which is deliberately tiny `copy.replace()` looks up `__replace__` on `type(obj)`, calls it with the keyword changes, and returns whatever comes back. If the class has no such method it raises `TypeError: replace() does not support X objects`. That is the entire function — every decision about *how* to build the new object belongs to the class. The implication worth stating in an interview is that `copy.replace()` has no idea what a "field" is. It does not read `__dict__`, does not know about slots or annotations, and cannot validate a name. When a dataclass rejects `copy.replace(w, misses=1)`, the error is `Window.__init__() got an unexpected keyword argument 'misses'` — it came from the generated `__replace__` calling the real constructor, not from `copy.replace()` itself. ## Why it lives in `copy` but is not a copy hook `copy.replace()` shares a module with `copy.copy()` and `copy.deepcopy()`, and that is the only thing it shares with them. It never looks at `__copy__` or `__deepcopy__`; it recurses into nothing; there is no memo, because there is no graph walk to terminate. Unchanged fields are carried into the new object by reference, exactly as a constructor call would carry them, so a nested mutable list is shared between the original and the result. If you need the result independent, deep-copy it separately — the two operations do not compose implicitly. The distinction also runs the other way. `copy.copy()` on a frozen dataclass gives an equal object without running validation; `copy.replace()` builds through the class's own constructor, so `__post_init__` and any invariant checks run again on the new values. For a type whose validity depends on relationships between fields, that difference is the point rather than a detail. ## Implementing `__replace__` yourself For a hand-written class the implementation is a constructor call with the current values as the base and the changes layered on top: ```python def __replace__(self, /, **changes): fields = {"offset": self.offset, "batch": self.batch} fields.update(changes) return type(self)(**fields) ``` Use `type(self)` so subclasses come back as themselves, and let the constructor reject unknown keywords rather than validating names yourself — a spurious hand-rolled check is one more thing to keep in step. Define it only where a functional update genuinely makes sense: a value object, an immutable configuration record, a cursor. On a mutable service object with a lifecycle it is misleading, because callers will read `copy.replace()` as cheap when it may be rebuilding real state. ## What to say about portability Because it landed in 3.13, code that must also run on 3.12 or earlier cannot call `copy.replace()`. There is no backport in the standard library; on older interpreters you call the per-type spelling directly — `dataclasses.replace()`, `_replace()`, or the type's own `replace()` method. Defining `__replace__` on your own class is harmless on any version: on 3.12 it is simply a method nothing calls, and it becomes live the moment the code runs on 3.13 or later. This is a small, genuinely modern corner of the language. Nobody's interview turns on it, but knowing it exists — and being able to say precisely how it differs from a copy — reads as someone who follows what the language is doing rather than someone who stopped reading changelogs at 3.8. ## A last practical note Because the protocol is a single dunder, adding support to a class you already own is a three-line change, and it costs nothing at import time. The reason to hesitate is not cost but meaning: `copy.replace()` reads as "the same thing with one field different", and a type whose identity includes something the constructor cannot reproduce — a database row id, an open handle, a registration in a global table — should not offer it, because the object it hands back will look interchangeable with the original and will not be.
- How does `copy.replace()` differ from `copy.copy()` on a frozen dataclass?`copy.copy()` reproduces the object as it is, without running the constructor, so no validation happens and every field is identical. `copy.replace()` goes through the class's own `__replace__`, which for a dataclass calls the real constructor — so `__post_init__` and any invariant checks run again on the substituted values. One is a duplicate, the other is a rebuild that happens to reuse most of the inputs.
- Does `copy.replace()` deep-copy the fields it is not changing?No. It copies nothing recursively; unchanged fields are passed into the new object by reference exactly as a constructor call would pass them. A nested mutable list therefore stays shared between the original and the result, and mutating it is visible through both. If you need independence, deep-copy explicitly — `copy.replace()` and `copy.deepcopy()` do not compose implicitly.
- Which standard-library types support `copy.replace()` today?Dataclasses and named tuples, the `datetime` types `date`, `time` and `datetime`, `inspect.Signature` and `inspect.Parameter`, and `types.SimpleNamespace`, among others — each gained a `__replace__` method in Python 3.13. A plain user-defined class supports nothing until it defines `__replace__` itself; calling `copy.replace()` on one raises `TypeError`.
saying these in an interview costs you the question
- Thinks it deep-copies the untouched fields
- Expects it to work on any object with attributes
- Says it mutates the original in place
- Believes it consults `__copy__` or `__deepcopy__`
- Assumes it is available on Python 3.12