Skip to content

Mutable vs Immutable Types

Every Python object has a fixed identity (its address in memory) and a type. Some types allow the object's internal state to change after creation; others do not. This distinction---mutable versus immutable---is one of the most important mental models in the language.

Think of it this way: a mutable object is like a whiteboard. You can erase parts, write new content, and the board itself remains the same physical object. An immutable object is like a printed page. To "change" it, you must create an entirely new page; the original stays as it was.

Mental Model

Mutable objects can be changed after creation; immutable objects cannot. When you "modify" an immutable value like a string, Python actually creates a brand-new object and rebinds the name to it. This single distinction determines whether aliases share changes and whether a type can serve as a dictionary key.


1. Classification of Built-in Types

Python's core types split cleanly into two groups.

Mutable Immutable
list int
dict float
set str
bytearray tuple
frozenset
bytes
bool

The rule is simple: if the type provides methods or operations that modify the object in place, it is mutable. If every "modification" produces a new object, it is immutable.


2. What Immutability Means Precisely

An immutable object's state cannot change after creation. The object continues to exist at the same memory address, but no operation can alter its contents.

```python s = "hello" print(id(s))

s = s + " world" print(id(s)) ```

Output:

text 140234866482992 140234866483056

The two id values differ. The string "hello" was never modified. Instead, s + " world" created a brand-new string object, and the name s was rebound to point at it. The original "hello" object still exists unchanged (until garbage collected).


3. Attempting to Modify an Immutable Object

Trying to mutate an immutable object raises a TypeError.

python t = (1, 2, 3) t[0] = 99

Output:

text TypeError: 'tuple' object does not support item assignment

The same applies to strings.

python s = "hello" s[0] = "H"

Output:

text TypeError: 'str' object does not support item assignment

Python does not silently ignore the attempt---it raises an error immediately.


4. Mutable Objects Can Change in Place

Mutable objects support operations that alter their internal state without creating a new object.

```python nums = [10, 20, 30] print(id(nums))

nums.append(40) print(id(nums)) print(nums) ```

Output:

text 140234866520000 140234866520000 [10, 20, 30, 40]

The id stays the same. The list object itself was modified; no new list was created.


5. The Subtlety: Tuples Containing Mutable Elements

A tuple is immutable, meaning you cannot add, remove, or reassign its elements. However, if one of those elements is itself a mutable object, the mutable object's contents can still change.

python pair = ([1, 2], [3, 4]) pair[0].append(99) print(pair)

Output:

text ([1, 2, 99], [3, 4])

The tuple pair still holds the same two list objects it held at creation. The tuple itself has not changed---it still contains references to the same objects at the same memory addresses. But the list that pair[0] refers to is mutable, so its contents can change.

This is a common source of confusion. The tuple's guarantee is about its own structure (which slots point to which objects), not about the internal state of the objects in those slots.

What you cannot do is reassign a slot in the tuple.

python pair = ([1, 2], [3, 4]) pair[0] = [10, 20]

Output:

text TypeError: 'tuple' object does not support item assignment


6. Augmented Assignment and Tuples

The += operator on a tuple element that is a list reveals this subtlety in a surprising way.

python t = ([1, 2],) try: t[0] += [3] except TypeError as e: print(e) print(t)

Output:

text 'tuple' object does not support item assignment ([1, 2, 3],)

Both things happen: the list is mutated in place (because list.__iadd__ extends the list), and a TypeError is raised (because the tuple rejects the assignment back to slot 0). The list ends up modified even though the operation raised an error.


7. Checking Mutability with id

A practical way to test whether an operation mutates an object or creates a new one is to compare id values before and after.

```python

Immutable: id changes

x = 10 print(id(x)) x += 1 print(id(x))

Mutable: id stays the same

y = [10] print(id(y)) y.append(20) print(id(y)) ```

Output:

text 140234866389008 140234866389040 140234866520064 140234866520064

For the integer, x += 1 created a new int object. For the list, y.append(20) modified the existing object.


8. Summary

Key ideas:

  • Immutable types (int, str, tuple, frozenset, bytes, bool, float) cannot be changed after creation. Any "modification" produces a new object.
  • Mutable types (list, dict, set, bytearray) can be changed in place. The object's identity stays the same.
  • A tuple is immutable in its structure, but if it contains mutable elements, those elements can still be modified.
  • Attempting to mutate an immutable object raises TypeError.
  • Use id() to verify whether an operation creates a new object or modifies the existing one.

Exercises

Exercise 1. Predict the output of the following code. Explain why the id comparison produces the result it does.

```python a = "Python" b = a a = a.upper()

print(a) print(b) print(a is b) ```

Solution to Exercise 1

Output:

text PYTHON Python False

Strings are immutable. a.upper() does not modify the original string "Python". Instead, it creates a new string "PYTHON" and rebinds the name a to point at it. The name b still refers to the original "Python" object. Since a and b now point to different objects, a is b evaluates to False.


Exercise 2. Consider this code:

python t = (1, [2, 3], "hello") t[1].append(4) print(t)

Does this raise a TypeError? Why or why not? What would happen if you tried t[2] = "world" instead?

Solution to Exercise 2

No, it does not raise a TypeError. The output is:

text (1, [2, 3, 4], 'hello')

The tuple t is immutable---you cannot reassign its elements. But t[1] is a list, which is mutable. Calling t[1].append(4) modifies the list object that the tuple references. The tuple's structure has not changed: slot 1 still points to the same list object (same id). Only the list's internal contents have changed.

If you tried t[2] = "world", Python would raise:

text TypeError: 'tuple' object does not support item assignment

This fails because it attempts to change which object the tuple's slot 2 refers to, which is exactly what tuple immutability prevents.


Exercise 3. A programmer writes this function intended to toggle a flag stored as an integer and also append to a log list:

```python def update(flag, log): flag += 1 log.append("updated")

flag = 0 log = [] update(flag, log)

print(flag) print(log) ```

Predict the output. Explain why flag is unchanged after the call but log is not.

Solution to Exercise 3

Output:

text 0 ['updated']

Inside the function, flag += 1 creates a new integer object (because int is immutable) and binds the local name flag to it. The caller's flag variable still points to the original integer 0. Rebinding a local name has no effect on the caller.

In contrast, log.append("updated") mutates the existing list object (because list is mutable). Both the caller's log and the function's log refer to the same list object, so the change is visible after the function returns.

This is a critical distinction: passing a mutable object to a function allows the function to modify it in place, while passing an immutable object does not. Python's calling convention is "pass by object reference"---the function receives a reference to the same object, but rebinding the local name does not affect the caller.