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:

  1. Mutable Objects: Objects that can change their state or contents after they are created.

  2. 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" to s, a new string object is created because strings are immutable.

  • The id() function shows that the memory address (identity) of s 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 and b reference the same integer object 10.

  • When b is modified, it now references a new integer object 15, leaving a 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 and lst2 reference the same list.

  • Modifying lst2 affects lst1 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 affect original.

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 and 256 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

  1. Avoid Mutable Default Arguments:

     def func(a, data=None):
         if data is None:
             data = []
    
  2. Use Immutable Objects When Possible:

    • For thread safety and to prevent unintended side effects.
  3. Understand Object References:

    • Know when variables reference the same object or different objects.
  4. Use Copying Appropriately:

    • Use copy.copy() or copy.deepcopy() to avoid unintended sharing of mutable objects.
  5. 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.

0
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

Sai Prasanna Maharana
Sai Prasanna Maharana