Home/Learn/Python A–Z/Mutability, Shallow Copy & Deep Copy

Mutability, Shallow Copy & Deep Copy

Intermediate
Data Structures

Assignment shares, copy() duplicates one level, deepcopy() duplicates everything — knowing which you need prevents the most common "my data changed by itself" bugs.

Overview

Python never copies on assignment — it shares references. That is fine for immutable objects (int, str, tuple) because they cannot change under you, but for lists/dicts/sets and objects, three levels exist: assignment (same object), shallow copy (new container, SAME inner objects), and deep copy (recursively new everything). Function arguments follow the same rule — mutable arguments can be modified by the callee, a behaviour interviewers probe with "is Python pass-by-value or pass-by-reference?" (answer: pass-by-object-reference).

Three Levels: Assign vs copy() vs deepcopy()

A shallow copy is a new outer container whose slots point to the same inner objects — mutating a NESTED item is visible through both. deepcopy severs everything.

Where shallow copy surprises you: nesting
import copy

teams = [["asha", "ravi"], ["neha"]]

alias   = teams                 # level 0: same object
shallow = teams.copy()          # level 1: new list, same inner lists
deep    = copy.deepcopy(teams)  # level 2: everything new

teams[1].append("kiran")        # mutate a NESTED list

print(alias[1])     # ['neha', 'kiran'] — same object, obviously
print(shallow[1])   # ['neha', 'kiran'] — SHALLOW shares inner lists!
print(deep[1])      # ['neha']          — deep copy unaffected

# Shallow copy spellings (equivalent):
a = teams.copy(); b = teams[:]; c = list(teams)

# For flat lists of immutables, shallow is all you need
nums = [1, 2, 3]
safe = nums.copy()
safe.append(4)      # nums untouched

Mutable Arguments — Pass by Object Reference

The callee receives the same object. Mutating it (append) affects the caller; REBINDING the parameter (=) does not. Functions that mutate inputs should say so — or copy first.

Mutation escapes the function; rebinding does not
def add_bonus(scores):        # receives the SAME list object
    scores.append(100)         # caller sees this!

def replace(scores):
    scores = [0, 0]            # rebinds LOCAL name only — caller unaffected

marks = [80, 90]
add_bonus(marks)
print(marks)                   # [80, 90, 100] — mutated!
replace(marks)
print(marks)                   # [80, 90, 100] — rebinding didn't escape

# Defensive pattern — don't surprise your caller
def normalized(scores):
    result = scores.copy()     # work on a copy
    result.sort()
    return result

# int/str/tuple arguments are safe — immutable objects can't change

Key Points to Remember

  • 1Assignment NEVER copies — it binds another name to the same object
  • 2Shallow copy (copy(), [:], list()) duplicates one level; deepcopy() recurses
  • 3Python is pass-by-object-reference: callees can mutate, not rebind, your objects
  • 4Immutables (int, str, tuple, frozenset) are immune to all of this — a reason to prefer them

Interview Questions

Sign in to ask Aria
1

Is Python pass-by-value or pass-by-reference? Demonstrate with a list.

MediumThoughtworks
2

Shallow vs deep copy — construct an example where shallow copy is a bug.

MediumWalmart
3

Why is a mutable default argument dangerous, given what you know about references?

HardGoogle

Ask Aria about Mutability, Shallow Copy & Deep Copy

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…