runtime
177 / 250
lesson 177 Memory & Python Internals · advanced

Shallow and deep copies

A copy can be one level deep (the outer container is new, everything inside is shared) or all the way down. Choosing the wrong one leaks changes into data you meant to keep.

You'll be able to
  • Name the shallow copies (copy.copy, list(), dict(), slices, .copy()) and what they share
  • Use copy.deepcopy for independent nested data, and know it preserves shared parts and cycles
  • Decide when a shallow copy is enough and when a deep copy is needed
The idea

Every common way of copying a container is shallow: copy.copy(x), list(x), x[:], x.copy(), dict(x). It creates a new outer container whose slots refer to the same inner objects. Changing the copy's top level (adding, removing, replacing an entry) is safe; changing an object inside it changes the original's too.

copy.deepcopy(x) copies recursively, so nothing mutable is shared with the original.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Tenant settings from a template
import copy

template = {"plan": "standard", "limits": {"users": 5, "storage_gb": 10}}
shallow = copy.copy(template)
deep = copy.deepcopy(template)
shallow["plan"] = "pro"
shallow["limits"]["users"] = 50
print(template)
print(deep["limits"]["users"], shallow["limits"] is template["limits"], deep["limits"] is template["limits"])

deepcopy is smarter than *copy everything*: it remembers what it has already copied (a memo), so an object referenced twice in the original is copied once and referenced twice in the copy, and a structure that refers to itself (a cycle) doesn't send it round forever.

example-2.py · Shared parts stay shared, cycles survive
import copy

address = {"city": "Houston", "state": "TX"}
customer = {"id": "C00077", "billing": address, "shipping": address}
clone = copy.deepcopy(customer)
clone["shipping"]["city"] = "Austin"
print(clone["billing"]["city"], customer["billing"]["city"])
print(clone["billing"] is clone["shipping"], clone["billing"] is address)

node = {"name": "loop"}
node["next"] = node
twin = copy.deepcopy(node)
print(twin["next"] is twin, twin is node)

When to use which: a shallow copy when you only add, remove or replace top-level entries (sorting a copy of a list of records, filtering a dictionary). A deep copy when you will change something nested and the original must stay as it was (a template, a saved snapshot, test fixtures). Immutable values (numbers, strings, tuples of immutables) are never a problem: they can't change, so sharing them is free. Mistakes to avoid: using list(rows) or dict(settings) and then editing inside them; and deep-copying large data out of habit, which costs time and memory for every object.

Review
  • copy.copy, list(), dict(), slices and .copy() are shallow: a new outer container, shared insides.
  • copy.deepcopy copies every level, keeps doubly referenced parts shared once, and handles cycles.
  • Shallow is enough for top-level changes; deep is needed before changing something nested.
  • Next: objects that refer to each other in a cycle, which reference counting alone never frees.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Shallow and deep copies — Memory & Python Internals · Runtime