Python Mutable vs. Immutable Objects: A Comprehensive Guide

Python’s dynamic typing and object-oriented nature make it a flexible language, but one of its fundamental distinctions lies in how it handles mutable and immutable objects. This concept is critical for understanding how data behaves in memory, how it’s passed around in code, and how to avoid unexpected side effects. In this blog, we’ll dive deep into the differences between mutable and immutable objects, explore examples of each, examine their internal behavior, and discuss practical implications for Python developers.


What Are Mutable and Immutable Objects?

link to this section

In Python, every piece of data is an object, and objects are classified based on whether their state (value) can be changed after creation:

  • Mutable Objects : Objects whose state or content can be modified after they’re created.
  • Immutable Objects : Objects whose state cannot be changed once they’re created; any “modification” creates a new object.

This distinction affects how Python manages memory, handles assignments, and passes arguments to functions.


Mutable Objects

link to this section

Mutable objects can be altered in place, meaning you can change their contents without creating a new object. Common mutable types in Python include:

  • Lists (list)
  • Dictionaries (dict)
  • Sets (set)
  • Bytearrays (bytearray)
  • User-defined classes (unless explicitly made immutable)

Characteristics

  • In-Place Modification : Operations like appending, updating, or removing elements modify the existing object.
  • Shared References : Changes to a mutable object are visible to all references pointing to it.
  • Memory Efficiency : Modifying in place avoids creating new objects.

Example: Lists

my_list = [1, 2, 3] 
print(id(my_list)) # Memory address, e.g., 140712345678912 
my_list.append(4) # Modifies the list in place 
print(my_list) # Output: [1, 2, 3, 4] 
print(id(my_list)) # Same address as before
  • The id() function returns the object’s memory address, which remains unchanged because the list is modified in place.

Example: Dictionaries

my_dict = {"name": "Alice", "age": 25} 
my_dict["age"] = 26 # Updates the value
print(my_dict) # Output: {"name": "Alice", "age": 26}
  • The dictionary’s content changes, but it remains the same object.

Immutable Objects

link to this section

Immutable objects cannot be modified after creation. Any operation that seems to “change” them actually creates a new object. Common immutable types in Python include:

  • Integers (int)
  • Floats (float)
  • Strings (str)
  • Tuples (tuple)
  • Frozensets (frozenset)
  • Booleans (bool)

Characteristics

  • New Object Creation : Modifications result in a new object with a new memory address.
  • No Side Effects : Changes don’t affect other references, making them safer for sharing.
  • Hashable : Immutable objects can be used as dictionary keys or set elements (if they meet hashing criteria).

Example: Strings

my_string = "hello"
print(id(my_string)) # e.g., 140712345679456 
my_string = my_string + " world" # Creates a new string
print(my_string) # Output: "hello world"
print(id(my_string)) # Different address, e.g., 140712345680000
  • Concatenation doesn’t modify the original string; it creates a new one.

Example: Tuples

my_tuple = (1, 2, 3)
print(id(my_tuple)) # e.g., 140712345681024 
my_tuple = my_tuple + (4,) # Creates a new tuple
print(my_tuple) # Output: (1, 2, 3, 4)
print(id(my_tuple)) # Different address
  • Tuples are immutable, so “adding” elements generates a new tuple.

Internal Behavior: Memory and Identity

link to this section

Mutable Objects

  • Shared Identity : Multiple variables can reference the same mutable object. Changes via one reference affect all others.
  • Example :
    list1 = [1, 2, 3] 
    list2 = list1 # Points to the same object 
    list2.append(4)
    print(list1) # Output: [1, 2, 3, 4]
    print(list1 is list2) # Output: True (same object)

Immutable Objects

  • New Identity : Operations create new objects, leaving the original unchanged.
  • Example :
    str1 = "hello" 
    str2 = str1 
    str2 = str2 + " world"
    print(str1) # Output: "hello" (unchanged)
    print(str2) # Output: "hello world"
    print(str1 is str2) # Output: False (different objects)
  • Interning Exception : Small integers and some strings are interned (reused) for efficiency:
    a = 42 
    b = 42
    print(a is b) # Output: True (interned integer)

Mutable vs. Immutable: Key Differences

link to this section
Aspect Mutable Immutable
Can Change Yes, in place No, creates new object
Examples Lists, Dicts, Sets Strings, Tuples, Integers
Memory Address Stays the same Changes with modification
Hashable No (except frozenset) Yes
Side Effects Possible with shared refs None

Practical Implications

link to this section

1. Function Arguments

  • Mutable : Changes inside a function affect the original object.
    def modify_list(lst): 
        lst.append(4) 
        
    my_list = [1, 2, 3] 
    modify_list(my_list)
    print(my_list) # Output: [1, 2, 3, 4]
  • Immutable : The original object remains unchanged.
    def modify_string(s): 
        s = s + " world" 
    my_string = "hello" 
    modify_string(my_string)
    print(my_string) # Output: "hello"

2. Default Arguments in Functions

Using mutable objects as default arguments can lead to unexpected behavior:

def append_to_list(item, lst=[]): 
    lst.append(item) 
    return lst
print(append_to_list(1)) # Output: [1]
print(append_to_list(2)) # Output: [1, 2] (lst persists!)
  • The default lst is created once and reused, accumulating changes. Use None instead:
    def append_to_list(item, lst=None): 
        if lst is None: 
            lst = [] 
        lst.append(item) 
        return lst

3. Dictionary Keys

  • Only immutable objects can be dictionary keys:
    my_dict = {(1, 2): "tuple"} # Works (tuple is immutable) 
    my_dict[[1, 2]] = "list" # Raises TypeError (list is mutable)

4. Performance

  • Mutable : Modifying in place is efficient for large data (e.g., appending to a list).
  • Immutable : Repeated modifications (e.g., string concatenation) create new objects, which can be slower:
    s = ""
    for i in range(1000):
        s += str(i)  # Creates 1000 new strings
    # Better: Use a list and join
    lst = []
    for i in range(1000):
        lst.append(str(i))
    s = "".join(lst)

Edge Cases

link to this section

Tuples with Mutable Elements

Tuples are immutable, but they can contain mutable objects:

my_tuple = ([1, 2], 3) 
my_tuple[0].append(3) # Modifies the list inside
print(my_tuple) # Output: ([1, 2, 3], 3)
  • The tuple’s structure (references) is fixed, but the mutable list inside can change.

Custom Classes

User-defined classes are mutable by default but can be made immutable using techniques like __slots__ or overriding setters:

class ImmutablePoint:
    __slots__ = ["x", "y"]
    def __init__(self, x, y):
        self.x = x
        self.y = y

pt = ImmutablePoint(1, 2)
# pt.x = 3  # Raises AttributeError

Best Practices

link to this section
  1. Use Immutable When Possible : For safety and predictability (e.g., as dictionary keys or in sets).
  2. Copy Mutable Objects : Use copy.copy() or copy.deepcopy() to avoid unintended side effects:
    import copy 
          
    lst1 = [1, 2, 3] 
    lst2 = copy.copy(lst1) 
    lst2.append(4)
    print(lst1) # Output: [1, 2, 3]
  3. Be Cautious with Defaults : Avoid mutable defaults in functions.
  4. Optimize Performance : Use mutable types for in-place changes, immutable types for fixed data.

Conclusion

link to this section

The distinction between mutable and immutable objects is a cornerstone of Python programming. Mutable objects like lists and dictionaries offer flexibility for dynamic data, while immutable objects like strings and tuples provide safety and consistency. By understanding their behavior—how they’re stored, modified, and passed—you can write more robust, efficient, and bug-free code. Whether you’re designing data structures or debugging side effects, this knowledge is a powerful tool in your Python toolkit.