Understanding Python Objects: Mutable vs Immutable🐍
Python is a versatile language renowned for its simplicity and readability. A fundamental concept in Python is that everything is an object. Grasping how Python handles these objects, especially the differences between mutable and immutable types, is crucial for writing efficient and bug-free code.
id and type
Every object in Python has an identity, a type, and a value.
x = 42
print(f"Value: {x}")
print(f"Type: {type(x)}")
print(f"ID: {id(x)}")
Output:
Value: 42
Type: <class 'int'>
ID: 9794400
Assignment vs Referencing
In Python, variables are references to objects stored in memory. When you assign one variable to another, you are copying the reference, not the actual object. This means both variables point to the same object.
a = [1, 2, 3]
b = a
Here, both a and b reference the same list object in memory. Modifying the object through one reference affects the other.
To create a new object, you need to explicitly copy it:
a = [1, 2, 3]
b = a.copy()
b.append(4)
print(f"a: {a}") # Output: [1, 2, 3]
print(f"b: {b}") # Output: [1, 2, 3, 4]
Aliasing
When two or more variables reference the same object, it's known as aliasing. Any changes made through one alias affect the object as seen through the other aliases.
list1 = [1, 2, 3]
list2 = list1
list2.append(4)
print(list1) # Output: [1, 2, 3, 4]
Here, list1 and list2 are aliases for the same list object.
Mutable Objects
Mutable objects are those whose value can change after they are created. Common mutable types include:
Example with Lists
my_list = [1, 2, 3]
print(f"Original List: {my_list}, ID: {id(my_list)}")
my_list.append(4)
print(f"Modified List: {my_list}, ID: {id(my_list)}")
Output:
Original List: [1, 2, 3], ID: 140065346064640
Modified List: [1, 2, 3, 4], ID: 140065346064640
The id remains the same, indicating the same object has been modified.
Immutable Objects
Immutable objects cannot change their value once they are created. Common immutable types include:
Example with Strings
my_string = "Hello"
print(f"Original String: '{my_string}', ID: {id(my_string)}")
my_string += " World
print(f"Modified String: '{my_string}', ID: {id(my_string)}")
Output:
Original String: 'Hello', ID: 140065346771440
Modified String: 'Hello World', ID: 140065346772720
The id changes because a new string object is created when we modify it.
Special Case: Tuples and Frozen Sets
While tuples and frozen sets are immutable (you cannot add or remove items), they can contain mutable objects. This means the contents of the mutable elements can change, even though the tuple or frozen set itself cannot.
mutable_list = [1, 2, 3]
my_tuple = (mutable_list, 4, 5)
mutable_list.append(6)
print(my_tuple) # Output: ([1, 2, 3, 6], 4, 5)
In this example, although my_tuple is immutable, the list inside it (mutable_list) is mutable and can be modified.
Memory Storage of Immutable Objects
Immutable objects are stored in memory in such a way that any attempt to modify them results in the creation of a new object. This means that variables referencing immutable objects will point to different memory locations if their values change.
x = 1000
print(f"ID of x before change: {id(x)}")
x += 1
print(f"ID of x after change: {id(x)}")
Output:
ID of x before change: 140065346773040
ID of x after change: 140065346773360
As you can see, x now references a new object in memory.
Memory Schema Examples
Mutable Object Example
Consider the following code:
a = [1, 2, 3]
b = a
b.append(4)
Memory Schema:
a ----> [1, 2, 3, 4] <---- b
Both a and b reference the same list object in memory. Immutable Object Example Now, consider this code:
x = 10
y = x
y += 5
Memory Schema:
x ----> 10
y ----> 15
Initially, both x and y reference the integer 10. After y += 5, y references a new integer object 15, while x remains unchanged.
Integer Pre-allocation in CPython
For optimization purposes, CPython pre-allocates a range of small integers (from -5 to 256) at startup. These integers are singletons and are reused throughout the program.
a = 256
b = 256
print(a is b) # Output: True
c = 257
d = 257
print(c is d) # Output: False
This shows that integers within the pre-allocated range point to the same memory location, while those outside do not.
The range of pre-allocated integers is determined by the constants NSMALLPOSINTS (number of small positive integers) and NSMALLNEGINTS (number of small negative integers) defined in CPython's source code.
These constants dictate how many small integers are pre-allocated. For example, NSMALLPOSINTS is set to 257 to pre-allocate integers from 0 to 256, and NSMALLNEGINTS is set to 5 to pre-allocate integers from -1 to -5.
These values are chosen because small integers are frequently used in programs. Pre-allocating them improves performance and reduces memory usage by avoiding the creation of multiple identical objects.
Why Does It Matter?
Understanding mutability is vital because it affects:
How Python Treats Mutable and Immutable Objects Differently
Assignment and Modification
When you assign one variable to another, both reference the same object.
With Mutable Objects
a = [1, 2, 3]
b = a
b.append(4)
print(f"a: {a}")
print(f"b: {b}")
Output:
a: [1, 2, 3, 4]
b: [1, 2, 3, 4]
Both a and b point to the same list, so changes to one affect the other.
With Immutable Objects
x = 10
y = x
y += 5
print(f"x: {x}")
print(f"y: {y}")
Output:
x: 10
y: 15
x remains unchanged because integers are immutable; y now references a new object.
Function Arguments and Their Implications
Variable Passing in Functions
In Python, when you pass variables to functions, you are passing references to the objects (also known as "pass by assignment"). The function receives a reference to the same object, and the behavior differs for mutable and immutable objects.
For mutable objects:
def modify_list(lst):
lst.append(100)
numbers = [1, 2, 3]
modify_list(numbers)
print(numbers) # Output: [1, 2, 3, 100]
The original list numbers is modified inside the function.
For immutable objects:
def modify_number(n):
n += 10
print(f"Inside function: {n}") # Output: 15
number = 5
modify_number(number)
print(f"Outside function: {number}") # Output: 5
The original number remains unchanged because integers are immutable.
Mutable Default Arguments
Using mutable objects as default arguments can cause unexpected behavior.
def add_item(item, lst=[]):
lst.append(item)
return lst
print(add_item(1))
print(add_item(2))
print(add_item(3))
Output:
[1]
[1, 2]
[1, 2, 3]
The default list lst retains its state between function calls.
Correct Approach
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
print(add_item(1))
print(add_item(2))
print(add_item(3))
Output:
[1]
[2]
[3]
Now, a new list is created each time the function is called.
Passing Arguments to Functions
Mutable Objects
def modify_list(lst):
lst.append(100)
numbers = [1, 2, 3]
modify_list(numbers)
print(numbers)
Output:
[1, 2, 3, 100]
The original list is modified because lst references the same object.
Immutable Objects
def modify_number(n):
n += 10
print(f"Inside function: {n}")
number = 5
modify_number(number)
print(f"Outside function: {number}")
Output:
Inside function: 15
Outside function: 5
The original number remains unchanged.
Conclusion
Understanding the distinction between mutable and immutable objects in Python is essential for:
• Avoiding Bugs: Prevent unintended side effects in your code.
• Writing Efficient Code: Choose the appropriate data structures for your needs.
• Effective Function Design: Manage default arguments and object references properly.
By keeping these concepts in mind, you can harness the full power of Python’s object model to write clean, efficient, and maintainable code.