Mutable vs Immutable Objects

A comprehensive guide on understanding mutable vs. immutable objects in Python, memory addresses, and object identity.

Mutable vs Immutable Objects

It can be confounding for Python beginners to spot the differences between mutable and immutable objects. Moreover, getting a clear grasp of the significance of this concept can challenge even intermediate-level programmers. In this article, we will go from rudimentary concepts to advanced practical examples to bring its importance to light.


1. Familiar Ground: Why Two Types of Data Structures?

Lists are mutable, while strings and tuples are immutable—a rule of thumb you must have encountered earlier. But why do these two types even exist?

Immutable means: once an object is created, its values cannot be changed.

For example, strings are immutable:

s = "ab"
print(id(s))  # Output: e.g., 140705...

s += "c"
print(id(s))  # Output: Different memory address!

Note that variables point to an address (HouseNo + StreetNo + Area) in memory (City) where the data ("ab") is stored.

So, s was pointing to the memory location where "ab" was stored. Upon modification, it pointed somewhere else where "abc" was stored.

Variable s (just a name) pointed to some place on Google Maps, but now it points to a different place entirely. In programming, that means it is a different object. Hence, strings are immutable. If you think you changed the object’s value, think again—you changed the object itself!

What about a list?

lst = ["a", "b"]
print(id(lst))

lst.append("c")
print(id(lst))  # Same memory address!

Both IDs are the same. A list is mutable.


2. The Assignment Operator & Under the Hood Mechanics

There are many trick questions built on this concept:

a = [1, 2, 3]
b = a 
b.append(4)

print(a == b)

What is the output of print(a == b)? Even if you have read my blog up to this point, chances are you might answer False.

To get a deeper understanding of mutable and immutable objects, it is better to view a as an object of the list class defined in Python. The root of the confusion becomes clearer when we look at the methods this class must have.

  • When you create an object, its unique id is generated. This is true for both mutable and immutable objects.
  • A mutable object’s class has methods for in-place modification (e.g., adding or removing elements) without changing the object’s id created during initialization.
  • An immutable object’s class does not provide methods for in-place modification, meaning you couldn’t modify it even if you wanted to!

For the statement b = a, remember what = means in Python: it is an assignment, not a mathematical “equal to” sign. It simply means that whatever memory location a is pointing to is now also assigned to variable b.

Executing b.append(4) modifies whatever b is pointing to. Since b points to a list object, the modification does not change the id of that list. In our Google Maps analogy, the address did not change—the building just had a floor added to it!

Since a was pointing to that exact same location, evaluating a == b returns True.

Variable pointing to Mr ListA


3. Data Structure Mutability Reference

Investigating every data structure in Python to check its mutability takes time. Here is a quick breakdown:

MutableImmutable
listtuple
dictstr
setint, float

4. Edge Cases & Common Pitfalls

Case A: List Concatenation vs. .append()

a = [1, 2]
b = a + [3]
a.append(3)

print(a == b)        # True 
print(id(a) == id(b)) # False

Note that the == operator compares values. Do a and b have the same values? Yes ([1, 2, 3]).

However, when you concatenate two lists using +, a new list is created and returned. Unlike .append(), concatenation creates a brand-new object in memory, so id(a) and id(b) do not match.


Case B: Tuples Containing Mutable Objects

a = tuple([[1, 2], 4, 5])

# Example 1
a[0] = 3           
# TypeError: 'tuple' object does not support item assignment

# Example 2
a[0].append(3)     
# Works without any error!

a[0] = 3 raises a TypeError because tuple elements cannot be reassigned.

However, a[0].append(3) raises no error. Why? A tuple can contain both mutable and immutable objects. a[0].append(3) modifies the internal content of the list inside the tuple, but it does not modify the memory address of the list stored at a[0]. The tuple still references the exact same list id.


5. Memory Pooling and Identity Behavior

Not all immutable objects behave identically under the hood. If you examine them closely, you will notice an interesting distinction between composite containers like tuples and primitive types like strings or numbers:

# Comparing Tuples
tuple_a = tuple(["a", "b"])
tuple_b = tuple(["a", "b"])

print(id(tuple_a) == id(tuple_b)) 
# Output: False

Now consider strings:

# Comparing Strings
str_a = str("ab")
str_b = "ab"

print(id(str_a) == id(str_b)) 
# Output: True

Why does this happen?

Tuples are containers designed to hold arbitrary data. Whenever you build a new tuple from a list, Python allocates a brand-new object in memory with its own unique id.

Strings and integers, on the other hand, benefit from memory optimization techniques in CPython known as small integer caching and string interning:

  1. Small Integer Caching: CPython pre-allocates and caches integer objects in the range **-5 to 256** when the interpreter starts up. When you write a = 3, variable a doesn’t create a new object; it simply references the pre-existing 3 in this internal pool. (Note: Larger numbers outside this range, like 1000, fall outside the cache and allocate new objects).
  2. String Interning: CPython automatically reuses memory for string literals that resemble valid Python identifiers (e.g., short ASCII strings without spaces or special characters). For these literals, Python keeps a single copy in a central dictionary and points identical variable references to the same address. (Note: Dynamically generated strings or strings containing spaces, like "hello world", generally allocate separate memory objects unless explicitly interned using sys.intern()).

[!NOTE] The examples in this section are meant to illustrate the concept of object identity, not to define a rule for every integer. You may sometimes notice that even large integers (such as 3000000) have the same id(). This happens because the Python compiler may reuse identical constants as an optimization. Since this is an implementation detail and can vary across Python versions and environments, don’t rely on id() or is to compare numbers. Use == when you want to compare values.

For a deeper explanation, see: https://discuss.python.org/t/inconsistency-in-the-output-of-id/11414


Summary

Data structures are designed with specific constraints to handle different scenarios efficiently. Mutability and immutability are foundational characteristics that dictate how data behaves when passed around, modified, or shared across your codebase.