Python’s copy.deepcopy() creates a fully independent clone of an object, traversing every nested element of the object graph. This comprehensive copying is convenient but expensive. Deepcopy must handle arbitrary Python objects, check for duplicates (using a memo dict to avoid copying shared objects), and even call custom __deepcopy__ methods if defined. In practice, this means a lot of Python-level operations and memory allocation. As one developer noted, deep-copying a game state (~1000 objects) consumed about 25% of the program’s runtime. In that case, deepcopy took several ms per call (versus microseconds for a shallow copy), which was clearly a bottleneck.
This overhead often makes deepcopy the slowest approach to duplicating data.
In a simple benchmark I created (see this gist), replicating complex objects, deepcopy was much slower than alternatives like serialization – using json/orjson or pickle to dump and load the object.
Relative performance:
shallow_copy : 1.00x slower
orjson_copy : 100.19x slower
pickle_copy : 146.97x slower
json_copy : 504.14x slower
deepcopy : 664.24x slowerPydantic-AI Agent Example: In the Pydantic-AI framework (an extension of Pydantic for AI Agents workflows), an expensive use of deepcopy was recently identified and fixed by Codeflash. The code is provided below. The agent kept a message history (a list of message objects). Originally, when the agent needed to modify the last message (for example, inserting the LLM’s final answer), it deep-copied the entire message history to avoid mutating the original. For long histories, this was costly. The optimization discovered by Codeflash changed this to copy only what’s necessary: they now shallow-copy the list and deep-copy just the last message object that needs modification. The code comment says it all: “Only do deepcopy when we have to modify”. By avoiding an unnecessary deep clone of all messages, this change greatly reduced the overhead of producing the final output. This is a prime example of how a small tweak (avoiding a full deepcopy of a large structure) yielded a big performance win. The new code is now 180x faster!
The slow original code with deepcopy:
def _set_output_tool_return(self, return_content: str) -> list[_messages.ModelMessage]:
2 """Set return content for the output tool.
3 Useful if you want to continue the conversation and want to set the response to the output tool call.
4 """
5 if not self._output_tool_name:
6 raise ValueError('Cannot set output tool return content when the return type is `str`.')
7 messages = deepcopy(self._state.message_history) # This is expensive
8 last_message = messages[-1]
9 for part in last_message.parts:
10 if isinstance(part, _messages.ToolReturnPart) and part.tool_name == self._output_tool_name:
11 part.content = return_content
12 return messages
13 raise LookupError(f'No tool call found with tool name {self._output_tool_name!r}.')The optimized code that only deepcopies the last message:
def _set_output_tool_return(self, return_content: str) -> list[_messages.ModelMessage]:
2 """Set return content for the output tool.
3 Useful if you want to continue the conversation and want to set the response to the output tool call.
4 """
5 if not self._output_tool_name:
6 raise ValueError('Cannot set output tool return content when the return type is `str`.')
7
8 messages = self._state.message_history
9 last_message = messages[-1]
10 for idx, part in enumerate(last_message.parts):
11 if isinstance(part, _messages.ToolReturnPart) and part.tool_name == self._output_tool_name:
12 # Only do deepcopy when we have to modify
13 copied_messages = list(messages)
14 copied_last = deepcopy(last_message) #Only deepcopy the last message
15 copied_last.parts[idx].content = return_content # type: ignore[misc]
16 copied_messages[-1] = copied_last
17 return copied_messages
18
19 raise LookupError(f'No tool call found with tool name {self._output_tool_name!r}.')Pydantic Models (Deepcopy vs.Rebuilding): Pydantic, a popular data validation library, provides a model_copy() method to duplicate model instances. Under the hood, this uses Python’s copy mechanisms. Users discovered that using model_copy() was significantly slower than rebuilding a new instance from the same fields. In one experiment, creating a new model bypassing the original’s fields to the constructor was ~25% faster than model_copy(). Updating a field via copy was even worse: m2 = m1.model_copy(); m2.bar= 5 took 2.819 s, whereas simply making a new model with the changed field took ~0.873s. The Pydantic team explained why: model_copy() endsup calling __copy__/__deepcopy__ internally, and due to Pydantic’s internal structure (privateattributes, extra fields, etc.), this adds overhead beyond a plain dataclass copy.In other words, the “safe” deep copy path in Pydantic incurred extra Python logic, making it slower than expected. The takeaway: even in high-level libraries, a deep clone of an object can be noticeably slower than creating afresh one, especially if validation or other logic is involved in copying.
Given the above, it’s clear that you should avoid deepcopy unless it’s truly necessary. Here are some strategies and cases to consider:
Python’s deepcopy is a powerful convenience, but power comes with a price. It naively clones everything, which in Python (with its dynamic typing and object overhead) can be extremely slow and memory-intensive. We’ve seen how this impacts real projects: from Pydantic’s data models to agent frameworks and standard library functions, uncritical use of deepcopy can multiply execution time or memory use. Unless you explicitly require an independent duplicate of a complex object graph, it’s often better to avoid deepcopy.
Instead, choose a strategy that fits your needs: make shallow copies, manually construct new objects, or copy only the bits that truly need copying. Verify if the data is truly mutable or could be treated as immutable. If you’re writing library code (like Pydantic or dataclasses), think about providing options – as Pydantic did with copy_on_model_validation settings to allow no-copy or shallow-copy modes. And always profile your code: if a piece of code feels unexpectedly slow, check if deepcopy (or similar wholesale copying) is the culprit. In many cases, eliminating or reducing deep copies can give an easy performance win, sometimes speeding things up by 2×, 5×, or more. Use deepcopy sparingly, and you’ll keep your Python code running efficiently while still maintaining correctness where it counts.
Join our newsletter and stay updated with the latest in performance optimization automation.


Join our newsletter and stay updated with fresh insights and exclusive content.
