Skip to content

Prefer copy.replace over dataclasses.replace (3.13) #18229

@Jeremiah-England

Description

@Jeremiah-England

Summary

Python 3.13 added the copy.replace function:

This new function can replace all of these from What's New 3.13:

Image

I like this new feature because it consolidates the various replace interfaces. Unfortunately, it is slower in most cases except for dataclasses. So I wouldn't necessarily recommend a lint rule encourage it across the board.

However, a lint rule for dataclasses would help this feature gain adoption, so more people would be aware of the consolidation the __replace__ dunder offers and use it in their codebases. And I think it is preferable in the case of dataclasses themselves to use the more general function.

Benchmark details

Note, this benchmark was generated by Gemini 2.5 pro.

Benchmarking with 100000 iterations per test.

--- FrozenDataclass ---
dataclasses.replace: 0.132023 seconds
copy.replace:        0.132923 seconds
copy.replace is 0.99x slower than dataclasses.replace


--- NonFrozenDataclass ---
dataclasses.replace: 0.092363 seconds
copy.replace:        0.092373 seconds
copy.replace is 1.00x slower than dataclasses.replace


--- datetime.datetime object ---
datetime.replace(): 0.006441 seconds
copy.replace():     0.024414 seconds
copy.replace is 0.26x slower than datetime.replace()


--- collections.namedtuple ---
namedtuple._replace(): 0.046155 seconds
copy.replace():        0.065800 seconds
copy.replace is 0.70x slower than namedtuple._replace()


--- datetime.date object ---
datetime.date.replace(): 0.006121 seconds
copy.replace():            0.021930 seconds
copy.replace is 0.28x slower than datetime.date.replace()


--- datetime.time object ---
datetime.time.replace(): 0.006259 seconds
copy.replace():            0.022275 seconds
copy.replace is 0.28x slower than datetime.time.replace()


--- inspect.Parameter ---
inspect.Parameter.replace(): 0.058636 seconds
copy.replace():                0.079787 seconds
copy.replace is 0.73x slower than inspect.Parameter.replace()


--- inspect.Signature ---
inspect.Signature.replace(): 0.060312 seconds
copy.replace():                0.085842 seconds
copy.replace is 0.70x slower than inspect.Signature.replace()


--- types.SimpleNamespace ---
SimpleNamespace (manual copy): 0.023630 seconds
copy.replace():                  0.029961 seconds
copy.replace is 0.79x slower than manual copy


--- types.CodeType (code object) ---
types.CodeType.replace(): 0.018685 seconds
copy.replace():             0.043076 seconds
copy.replace is 0.43x slower than types.CodeType.replace()
import collections
import copy
import datetime
import inspect
import types
from dataclasses import dataclass
from dataclasses import replace as dc_replace
from timeit import Timer


@dataclass(frozen=True)
class FrozenDataclass:
    a: int
    b: int
    c: str
    d: float
    e: tuple[int, int]


@dataclass(frozen=False)
class NonFrozenDataclass:
    a: int
    b: int
    c: str
    d: float
    e: tuple[int, int]


# Sample records
frozen_record = FrozenDataclass(a=1, b=2, c="test", d=3.14, e=(5, 6))
non_frozen_record = NonFrozenDataclass(a=1, b=2, c="test", d=3.14, e=(5, 6))

# how many repeats in each timing
iterations = 100_000

print(f"Benchmarking with {iterations} iterations per test.\n")

# --- Test FrozenDataclass ---
print("--- FrozenDataclass ---")
dc_timer_frozen = Timer(lambda: dc_replace(frozen_record, a=2))
# Assuming copy.replace is available (Python 3.13+)
cp_timer_frozen = Timer(lambda: copy.replace(frozen_record, a=2))

dc_time_frozen = dc_timer_frozen.timeit(number=iterations)
cp_time_frozen = cp_timer_frozen.timeit(number=iterations)

print(f"dataclasses.replace: {dc_time_frozen:.6f} seconds")
print(f"copy.replace:        {cp_time_frozen:.6f} seconds")
if cp_time_frozen != 0:
    print(
        f"copy.replace is {dc_time_frozen / cp_time_frozen:.2f}x {'faster' if dc_time_frozen > cp_time_frozen else 'slower'} than dataclasses.replace"
    )
print("\n")

# --- Test NonFrozenDataclass ---
print("--- NonFrozenDataclass ---")
dc_timer_non_frozen = Timer(lambda: dc_replace(non_frozen_record, a=2))
cp_timer_non_frozen = Timer(lambda: copy.replace(non_frozen_record, a=2))

dc_time_non_frozen = dc_timer_non_frozen.timeit(number=iterations)
cp_time_non_frozen = cp_timer_non_frozen.timeit(number=iterations)

print(f"dataclasses.replace: {dc_time_non_frozen:.6f} seconds")
print(f"copy.replace:        {cp_time_non_frozen:.6f} seconds")
if cp_time_non_frozen != 0:
    print(
        f"copy.replace is {dc_time_non_frozen / cp_time_non_frozen:.2f}x {'faster' if dc_time_non_frozen > cp_time_non_frozen else 'slower'} than dataclasses.replace"
    )
print("\n")

# --- Test datetime.datetime object ---
print("--- datetime.datetime object ---")
dt_object = datetime.datetime.now()

# Benchmark datetime.replace()
dt_native_timer = Timer(lambda: dt_object.replace(microsecond=100))
dt_native_time = dt_native_timer.timeit(number=iterations)

# Benchmark copy.replace() for datetime object
cp_dt_timer = Timer(lambda: copy.replace(dt_object, microsecond=100))
cp_dt_time = cp_dt_timer.timeit(number=iterations)

print(f"datetime.replace(): {dt_native_time:.6f} seconds")
print(f"copy.replace():     {cp_dt_time:.6f} seconds")
if cp_dt_time != 0:
    print(
        f"copy.replace is {dt_native_time / cp_dt_time:.2f}x {'faster' if dt_native_time > cp_dt_time else 'slower'} than datetime.replace()"
    )
print("\n")

# --- Test collections.namedtuple ---
print("--- collections.namedtuple ---")
Point = collections.namedtuple("Point", ["x", "y"])
nt_obj = Point(1, 2)

nt_native_timer = Timer(lambda: nt_obj._replace(x=100))
nt_native_time = nt_native_timer.timeit(number=iterations)

cp_nt_timer = Timer(lambda: copy.replace(nt_obj, x=100))
cp_nt_time = cp_nt_timer.timeit(number=iterations)

print(f"namedtuple._replace(): {nt_native_time:.6f} seconds")
print(f"copy.replace():        {cp_nt_time:.6f} seconds")
if cp_nt_time != 0:
    print(
        f"copy.replace is {nt_native_time / cp_nt_time:.2f}x {'faster' if nt_native_time > cp_nt_time else 'slower'} than namedtuple._replace()"
    )
print("\n")

# --- Test datetime.date object ---
print("--- datetime.date object ---")
date_obj = datetime.date(2023, 1, 1)

date_native_timer = Timer(lambda: date_obj.replace(day=15))
dt_native_date_time = date_native_timer.timeit(number=iterations)

cp_date_timer = Timer(lambda: copy.replace(date_obj, day=15))
cp_date_time = cp_date_timer.timeit(number=iterations)

print(f"datetime.date.replace(): {dt_native_date_time:.6f} seconds")
print(f"copy.replace():            {cp_date_time:.6f} seconds")
if cp_date_time != 0:
    print(
        f"copy.replace is {dt_native_date_time / cp_date_time:.2f}x {'faster' if dt_native_date_time > cp_date_time else 'slower'} than datetime.date.replace()"
    )
print("\n")

# --- Test datetime.time object ---
print("--- datetime.time object ---")
time_obj = datetime.time(12, 30, 0)

time_native_timer = Timer(lambda: time_obj.replace(minute=45))
dt_native_time_time = time_native_timer.timeit(number=iterations)

cp_time_timer = Timer(lambda: copy.replace(time_obj, minute=45))
cp_time_time = cp_time_timer.timeit(number=iterations)

print(f"datetime.time.replace(): {dt_native_time_time:.6f} seconds")
print(f"copy.replace():            {cp_time_time:.6f} seconds")
if cp_time_time != 0:
    print(
        f"copy.replace is {dt_native_time_time / cp_time_time:.2f}x {'faster' if dt_native_time_time > cp_time_time else 'slower'} than datetime.time.replace()"
    )
print("\n")

# --- Test inspect.Parameter ---
print("--- inspect.Parameter ---")
param_obj = inspect.Parameter("x", inspect.Parameter.POSITIONAL_OR_KEYWORD, default=42)

param_native_timer = Timer(lambda: param_obj.replace(default=100))
param_native_time = param_native_timer.timeit(number=iterations)

cp_param_timer = Timer(lambda: copy.replace(param_obj, default=100))
cp_param_time = cp_param_timer.timeit(number=iterations)

print(f"inspect.Parameter.replace(): {param_native_time:.6f} seconds")
print(f"copy.replace():                {cp_param_time:.6f} seconds")
if cp_param_time != 0:
    print(
        f"copy.replace is {param_native_time / cp_param_time:.2f}x {'faster' if param_native_time > cp_param_time else 'slower'} than inspect.Parameter.replace()"
    )
print("\n")

# --- Test inspect.Signature ---
print("--- inspect.Signature ---")
param_for_sig = inspect.Parameter("y", inspect.Parameter.POSITIONAL_ONLY)
sig_obj = inspect.Signature(parameters=[param_for_sig], return_annotation=int)
new_param_for_sig = inspect.Parameter("z", inspect.Parameter.KEYWORD_ONLY)

sig_native_timer = Timer(lambda: sig_obj.replace(parameters=[new_param_for_sig]))
sig_native_time = sig_native_timer.timeit(number=iterations)

cp_sig_timer = Timer(lambda: copy.replace(sig_obj, parameters=[new_param_for_sig]))
cp_sig_time = cp_sig_timer.timeit(number=iterations)

print(f"inspect.Signature.replace(): {sig_native_time:.6f} seconds")
print(f"copy.replace():                {cp_sig_time:.6f} seconds")
if cp_sig_time != 0:
    print(
        f"copy.replace is {sig_native_time / cp_sig_time:.2f}x {'faster' if sig_native_time > cp_sig_time else 'slower'} than inspect.Signature.replace()"
    )
print("\n")

# --- Test types.SimpleNamespace ---
print("--- types.SimpleNamespace ---")
sns_obj = types.SimpleNamespace(a=1, b="test")

# Native way to create a modified copy
sns_native_timer = Timer(lambda: types.SimpleNamespace(**{**sns_obj.__dict__, "a": 100}))
sns_native_time = sns_native_timer.timeit(number=iterations)

cp_sns_timer = Timer(lambda: copy.replace(sns_obj, a=100))
cp_sns_time = cp_sns_timer.timeit(number=iterations)

print(f"SimpleNamespace (manual copy): {sns_native_time:.6f} seconds")
print(f"copy.replace():                  {cp_sns_time:.6f} seconds")
if cp_sns_time != 0:
    print(
        f"copy.replace is {sns_native_time / cp_sns_time:.2f}x {'faster' if sns_native_time > cp_sns_time else 'slower'} than manual copy"
    )
print("\n")

# --- Test types.CodeType (code object) ---
print("--- types.CodeType (code object) ---")


def example_func_for_code_obj(x, y):
    return x + y


code_obj = example_func_for_code_obj.__code__

# Native types.CodeType.replace()
# Replacing co_name for a simple change. Other attributes might be more complex or restricted.
code_native_timer = Timer(lambda: code_obj.replace(co_name="new_func_name"))
code_native_time = code_native_timer.timeit(number=iterations)

# copy.replace() for types.CodeType
cp_code_timer = Timer(lambda: copy.replace(code_obj, co_name="new_func_name"))
cp_code_time = cp_code_timer.timeit(number=iterations)

print(f"types.CodeType.replace(): {code_native_time:.6f} seconds")
print(f"copy.replace():             {cp_code_time:.6f} seconds")
if cp_code_time != 0:
    print(
        f"copy.replace is {code_native_time / cp_code_time:.2f}x {'faster' if code_native_time > cp_code_time else 'slower'} than types.CodeType.replace()"
    )
print("\n")

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-decisionAwaiting a decision from a maintainerruleImplementing or modifying a lint rule

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions