Mutable vs Immutable Types

Difficulty: Advanced

In Python, every object is either mutable or immutable. Immutable objects -- such as int, float, str, tuple, frozenset, and bytes -- cannot be changed after creation. Any operation that appears to modify them actually creates a new object. Mutable objects -- such as list, dict, set, and bytearray -- can be changed in place without creating a new object.

This distinction matters enormously when you pass objects to functions, store them in data structures, or assign one variable to another. When you write `b = a` and `a` refers to a mutable object, both `a` and `b` point to the exact same object in memory. Mutating through one name is visible through the other. This is called aliasing and is one of the most common sources of bugs in Python.

The `copy` module provides two functions to deal with this. `copy.copy()` creates a shallow copy: it makes a new outer container but does not duplicate nested objects. `copy.deepcopy()` recursively copies every nested object, producing a fully independent clone. Understanding when to use each is critical for writing correct code.

A useful way to check identity vs equality is with the `is` operator (checks if two names point to the same object in memory) versus `==` (checks if two objects have the same value). For immutable types Python sometimes reuses objects (like small integers and interned strings), so `is` may return True even for separately created values, but you should never rely on this behavior.

In interviews, you will frequently be asked to predict the output of code that involves aliasing, default mutable arguments, or shallow copies of nested structures. Mastering these concepts separates confident Python developers from those who guess.

Code examples

Aliasing with mutable objects

a = [1, 2, 3]
b = a
b.append(4)
print("a:", a)
print("b:", b)
print("same object?", a is b)

b = a does not copy the list. Both names refer to the same list object, so appending through b also changes a.

Shallow copy vs deep copy

import copy

original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)

original[0].append(99)

print("original:", original)
print("shallow:", shallow)
print("deep:", deep)

Shallow copy creates a new outer list but inner lists are still shared. Deep copy creates fully independent nested objects.

Immutable types create new objects on modification

x = "hello"
print("id before:", id(x))
x = x + " world"
print("id after:", id(x))
print("value:", x)

Strings are immutable. Concatenation creates a brand new string object with a different id. The original string is unchanged and eventually garbage collected.

Tuple immutability with mutable contents

t = ([1, 2], [3, 4])
t[0].append(99)
print("tuple:", t)

try:
    t[0] = [5, 6]
except TypeError as e:
    print("error:", e)

The tuple itself is immutable (you cannot reassign its slots), but if a slot holds a mutable object like a list, that list can still be mutated in place.

Key points

Concepts covered

mutability, shallow copy, deep copy, aliasing