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?
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
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
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
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
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
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
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
- Use Immutable When Possible : For safety and predictability (e.g., as dictionary keys or in sets).
- 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]
- Be Cautious with Defaults : Avoid mutable defaults in functions.
- Optimize Performance : Use mutable types for in-place changes, immutable types for fixed data.
Conclusion
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.