Mutable and Immutable Objects in Python, Copying Objects, Interning and Caching
Mutable and Immutable Objects in Python
In Python, everything is an object, and these objects can be classified into two main categories based on their mutability:
Mutable Objects: Objects that can change their state or contents after they are created.
Immutable Objects: Objects that cannot change their state or contents once they are created.
Understanding the difference between mutable and immutable objects is crucial for writing efficient, bug-free code and for understanding how Python handles data structures internally.
Immutable Objects
Immutable objects are objects whose state cannot be modified after they are created. Any attempt to modify an immutable object results in the creation of a new object.
Examples of Immutable Objects
Numbers:
int
,float
,complex
Strings:
str
Tuples
Frozen Sets:
frozenset
Bytes:
bytes
Characteristics of Immutable Objects
Unchangeable State: Once an immutable object is created, its value cannot be altered.
Hashable: Immutable objects have a fixed hash value during their lifetime (
__hash__()
method doesn't change). Therefore, they can be used as keys in dictionaries or elements in sets.Thread-Safe: Since they cannot change, immutable objects are inherently thread-safe.
Example with Strings
# Immutable String Example
s = "hello"
print(id(s)) # Memory address of 's'
s += " world"
print(id(s)) # Different memory address
Explanation:
When we concatenate
" world"
tos
, a new string object is created because strings are immutable.The
id()
function shows that the memory address (identity) ofs
has changed, indicating a new object.
Example with Integers
# Immutable Integer Example
a = 10
print(id(a)) # Memory address of 'a'
a += 5
print(id(a)) # Different memory address
Explanation:
- Similar to strings, integers are immutable. Any arithmetic operation results in a new integer object.
Mutable Objects
Mutable objects are objects whose state or contents can be changed after creation.
Examples of Mutable Objects
Lists:
list
Dictionaries:
dict
Sets:
set
Byte Arrays:
bytearray
User-Defined Classes (unless explicitly made immutable)
Characteristics of Mutable Objects
Changeable State: You can modify the content or state of a mutable object without creating a new object.
Not Hashable: Most mutable objects cannot be hashed (
__hash__()
method may not be implemented or may change), so they cannot be used as dictionary keys or set elements.Potential Side Effects: Modifying a mutable object can have side effects if other variables reference the same object.
Example with Lists
# Mutable List Example
lst = [1, 2, 3]
print(id(lst)) # Memory address of 'lst'
lst.append(4)
print(id(lst)) # Same memory address
print(lst) # Output: [1, 2, 3, 4]
Explanation:
The
append()
method modifies the list in place.The
id()
remains the same, showing that the object itself hasn't changed, only its contents.
Implications of Mutability
Assignments and References
In Python, variables are references to objects. Understanding how references work with mutable and immutable objects is essential.
Immutable Objects
a = 10
b = a
print(a is b) # True
b += 5
print(a) # 10
print(b) # 15
print(a is b) # False
Explanation:
Initially,
a
andb
reference the same integer object10
.When
b
is modified, it now references a new integer object15
, leavinga
unchanged.
Mutable Objects
lst1 = [1, 2, 3]
lst2 = lst1
lst2.append(4)
print(lst1) # [1, 2, 3, 4]
print(lst2) # [1, 2, 3, 4]
print(lst1 is lst2) # True
Explanation:
lst1
andlst2
reference the same list.Modifying
lst2
affectslst1
because they point to the same object.
Function Arguments
When passing objects to functions, the mutability of the object determines whether the function can modify the original object.
Immutable Objects
def increment(x):
x += 1
return x
a = 10
b = increment(a)
print(a) # 10
print(b) # 11
Explanation:
a
remains unchanged because integers are immutable.The function
increment
creates a new integer object.
Mutable Objects
def append_element(lst):
lst.append(4)
my_list = [1, 2, 3]
append_element(my_list)
print(my_list) # [1, 2, 3, 4]
Explanation:
my_list
is modified in place by the function because lists are mutable.
Copying Objects
Due to the behavior of mutable objects, copying them requires attention to avoid unintended side effects.
Shallow Copy
Definition: Creates a new object but inserts references to the items found in the original.
Method:
copy.copy()
import copy
original = [1, 2, [3, 4]]
shallow_copied = copy.copy(original)
shallow_copied[2].append(5)
print(original) # [1, 2, [3, 4, 5]]
print(shallow_copied) # [1, 2, [3, 4, 5]]
Explanation:
- Both lists share the same nested list
[3, 4]
, so changes affect both.
Deep Copy
Definition: Creates a new object and recursively adds copies of nested objects found in the original.
Method:
copy.deepcopy()
import copy
original = [1, 2, [3, 4]]
deep_copied = copy.deepcopy(original)
deep_copied[2].append(5)
print(original) # [1, 2, [3, 4]]
print(deep_copied) # [1, 2, [3, 4, 5]]
Explanation:
- The nested list is copied, so changes to
deep_copied
do not affectoriginal
.
Immutable Containers with Mutable Elements
An immutable container can contain mutable elements. While the container cannot be modified (e.g., you cannot add or remove elements), the mutable elements within it can be changed.
Example with Tuples
tup = (1, [2, 3], 4)
tup[1].append(5)
print(tup) # Output: (1, [2, 3, 5], 4)
Explanation:
The tuple
tup
is immutable; we cannot change its structure.However,
tup[1]
references a list, which is mutable. We can modify the list.
Practical Use Cases
Dictionaries and Sets
Only immutable and hashable objects can be used as keys in dictionaries or elements in sets.
Mutable objects are not hashable by default.
# Valid dictionary key
my_dict = {(1, 2): "value"} # Tuple keys are valid
# Invalid dictionary key
my_list = [1, 2]
# my_dict = {my_list: "value"} # Raises TypeError: unhashable type: 'list'
Immutable Defaults in Functions
- Using mutable default arguments can lead to unexpected behavior.
def add_to_list(element, lst=[]):
lst.append(element)
return lst
print(add_to_list(1)) # [1]
print(add_to_list(2)) # [1, 2]
print(add_to_list(3)) # [1, 2, 3]
Explanation:
The default list
lst
is created once when the function is defined.Subsequent calls modify the same list.
Solution: Use
None
as the default and create a new list inside the function.
def add_to_list(element, lst=None):
if lst is None:
lst = []
lst.append(element)
return lst
print(add_to_list(1)) # [1]
print(add_to_list(2)) # [2]
print(add_to_list(3)) # [3]
Interning and Caching
Python may cache small immutable objects like small integers and short strings for efficiency.
a = 256
b = 256
print(a is b) # True
a = 257
b = 257
print(a is b) # Might be False
Explanation:
Integers between
-5
and256
are cached.For larger integers, new objects may be created.
Summary of Key Points
Immutable Objects:
Cannot be changed after creation.
Operations that seem to modify them return new objects.
Hashable: Can be used as dictionary keys or set elements.
Examples:
int
,float
,str
,tuple
,frozenset
,bytes
.
Mutable Objects:
Can be changed after creation.
Operations modify the object in place.
Not hashable: Cannot be used as dictionary keys (with exceptions).
Examples:
list
,dict
,set
,bytearray
, custom objects.
Implications:
Be cautious when passing mutable objects to functions; they can be modified unintentionally.
Use immutable objects for keys in dictionaries to ensure consistency.
Be mindful of default mutable arguments in functions.
Best Practices
Avoid Mutable Default Arguments:
def func(a, data=None): if data is None: data = []
Use Immutable Objects When Possible:
- For thread safety and to prevent unintended side effects.
Understand Object References:
- Know when variables reference the same object or different objects.
Use Copying Appropriately:
- Use
copy.copy()
orcopy.deepcopy()
to avoid unintended sharing of mutable objects.
- Use
Immutable Data Structures:
- Consider using immutable data structures (e.g., tuples, frozensets) when the data should not change.
Conclusion
Understanding mutable and immutable objects in Python is essential for:
Efficient Memory Usage: Immutable objects can be shared, reducing memory consumption.
Avoiding Bugs: Knowing how mutability affects data helps prevent unintended side effects.
Writing Clean Code: Proper use of mutable and immutable objects leads to clearer, more maintainable code.
Subscribe to my newsletter
Read articles from Sai Prasanna Maharana directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by