DEV Community

Cover image for Understanding Python's Object Model: Mutable vs Immutable Objects
Abdullah alshebel
Abdullah alshebel

Posted on

Understanding Python's Object Model: Mutable vs Immutable Objects

Introduction

In this post, I'll walk through key Python concepts I explored while learning more deeply about how objects, identity, and mutability work. We'll examine:

  • Python's id() and type() functions
  • What makes objects mutable or immutable
  • How this impacts program behavior
  • Why it matters when passing arguments to functions

Throughout, I'll provide simple code examples to make each concept concrete and relatable for new and intermediate Python learners.


Understanding id() and type()

Every object in Python has a unique identity that you can inspect using the id() function. You can also inspect the object's class with the type() function.

Basic Example

x = [1, 2, 3]
print(id(x))        # Outputs object's unique identifier (memory address)
print(type(x))      # <class 'list'>
Enter fullscreen mode Exit fullscreen mode

Output:

140234567890123
<class 'list'>
Enter fullscreen mode Exit fullscreen mode

Identity vs Equality

Even two variables holding identical contents may have different ids if they're different objects in memory. This helps distinguish identity (is) from equality (==):

a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)   # True, they have equal contents
print(a is b)   # False, they're different objects
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: == checks if values are equal, while is checks if they're the same object in memory.


Mutable Objects

A mutable object can have its internal state changed after it is created. Common examples include:

  • Lists
  • Dictionaries
  • Sets
  • Most classes with modifiable attributes

Example: Modifying a List

numbers = [1, 2, 3]
numbers.append(4)
print(numbers)  # [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Identity Persists After Modification

Changing the contents doesn't change the identity:

before = id(numbers)
numbers.append(5)
after = id(numbers)
print(before == after)  # True
print(numbers)          # [1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

Output:

True
[1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

Immutable Objects

Immutable objects cannot be changed in place after they're created. These include:

  • Integers
  • Floats
  • Strings
  • Tuples
  • Frozensets

Any 'modification' creates a new object.

Example: Integers

x = 5
print(id(x))
x += 1              # This rebinds x to a new object
print(id(x))        # The id is different now
Enter fullscreen mode Exit fullscreen mode

Output:

140734567823456
140734567823488  # Different id!
Enter fullscreen mode Exit fullscreen mode

Example: Strings

s = "hello"
print(id(s))
s += " world"
print(id(s))        # New object, new id
print(s)            # hello world
Enter fullscreen mode Exit fullscreen mode

Why Does It Matter? Python's Treatment of Mutable vs Immutable Objects

Understanding mutability influences everything from performance to subtle bugs. Python stores small integers and short strings in a cache, so they may appear to share ids in some cases, but in general, identity and mutability are fundamental to how variables work.

Key Differences

Aspect Mutable Objects Immutable Objects
Can be changed in-place? ✅ Yes ❌ No
Identity after modification Same Different
Safe to share references? ⚠️ Risky ✅ Safe
Examples list, dict, set int, str, tuple

Implications

Mutable objects:

  • Can be changed via various methods
  • If two variables reference the same object, changes by one variable affect the other

Immutable objects:

  • Safe to share
  • Reassigning a variable to a new value doesn't alter the original object

Example Comparison

# Mutable example
def modify_list(lst):
    lst.append(99)

l = [1, 2, 3]
modify_list(l)
print(l)  # [1, 2, 3, 99], because lists are mutable

# Immutable example
def modify_int(n):
    n += 1

x = 5
modify_int(x)
print(x)  # 5, unchanged
Enter fullscreen mode Exit fullscreen mode

Output:

[1, 2, 3, 99]
5
Enter fullscreen mode Exit fullscreen mode

How Arguments Are Passed to Functions

Python's argument passing is often summarized as "assignment by object reference" or "pass by object reference."

What This Means

  • For mutable types, functions can change the content of the argument outside the function
  • For immutable types, any operation that looks like a modification (e.g., x += 1) instead creates a new object and rebinds the local variable

More Examples

Mutable Example (List)

def add_element(mylist):
    mylist.append('new')

lst = [0]
add_element(lst)
print(lst)  # [0, 'new']
Enter fullscreen mode Exit fullscreen mode

Output: [0, 'new'] ✅ The original list was modified!

Immutable Example (Integer)

def add_one(num):
    num += 1
    print(f"Inside function: {num}")

n = 10
add_one(n)
print(f"Outside function: {n}")
Enter fullscreen mode Exit fullscreen mode

Output:

Inside function: 11
Outside function: 10
Enter fullscreen mode Exit fullscreen mode

❌ The original integer was NOT modified!

Why This Matters

This behavior is crucial to avoid unexpected side effects or bugs, especially when working with functions that handle lists or dictionaries.

Best Practices:

  • 🔍 Be aware of which types are mutable
  • 📝 Document if your function modifies arguments
  • 🛡️ Use .copy() if you need to avoid modifying the original

Advanced Tasks: Exploring Object Internals

While digging deeper, I explored several advanced concepts:

Topics Covered

  • ✅ Inspecting memory addresses of objects
  • ✅ Testing Python's integer caching (small integers cached for efficiency)
  • ✅ Careful copying: list.copy() vs slicing vs simple assignment

Copying Lists: Three Approaches

a = [1, 2, 3]

# Approach 1: Simple assignment (both refer to same object)
b = a
b.append(4)
print(f"a: {a}")   # [1, 2, 3, 4] - a was affected!

# Approach 2: Using .copy() (creates separate object)
c = a.copy()
c.append(5)
print(f"a: {a}")   # [1, 2, 3, 4] - unchanged
print(f"c: {c}")   # [1, 2, 3, 4, 5]

# Approach 3: Using slicing (also creates separate object)
d = a[:]
d.append(6)
print(f"a: {a}")   # [1, 2, 3, 4] - still unchanged
print(f"d: {d}")   # [1, 2, 3, 4, 6]
Enter fullscreen mode Exit fullscreen mode

Integer Caching Example

# Small integers are cached
x = 256
y = 256
print(x is y)  # True - same object!

# Large integers are not
a = 1000
b = 1000
print(a is b)  # False - different objects
Enter fullscreen mode Exit fullscreen mode

Key Lessons

  1. Assignment doesn't copy - it creates another reference
  2. Use .copy() or slicing to create independent copies
  3. Python caches small integers (-5 to 256) for efficiency
  4. Understanding object identity helps debug confusing behavior

Conclusion

Understanding Python's object model, especially the difference between mutable and immutable objects, is foundational for writing reliable, bug-free code. It impacts:

  • 🔧 Function arguments and side effects
  • 🎯 Variable assignment behavior
  • 📦 Copying data structures safely
  • ⚡ Performance optimizations

Summary Table

Concept Key Point
id() Returns unique object identifier
type() Returns object's class/type
Mutable Can be modified in-place (list, dict, set)
Immutable Cannot be modified (int, str, tuple)
Function Args Passed by object reference
Copying Use .copy() or [:] for independent copies

Practicing these patterns and observing object identities has fundamentally changed my perspective on how Python "really" works under the hood. I hope this guide helps you build a stronger mental model too! 🐍✨

Top comments (0)