Mutable and Immutable types in Python
In this post, I will review mutable and immutable types in Python. Understanding mutable and immutable types in Python will help you better understand how things work under the hood.
What are Mutable and Immutable types in Python?
In Python, an immutable object is one whose internal state cannot be modified after creation. Immutable types are not just a feature but a cornerstone of Python programming, providing predictability and safety for writing robust and secure code.
Once created, an immutable object ensures its value remains constant throughout its lifetime. This fundamental concept is crucial to grasp, as it clarifies that any operations that seem to modify the value of an immutable object will instead create a new object with the new value, leaving the original object unchanged.
Let's explore some basic mutable and immutable types in Python with some examples.
Immutable built-in types in Python
First, let's explore the immutable data types that Python provides out of the box.
Strings
String is one of the most common data types. Let's initialize a String and check its identity:
>>> val = "Hello, world!"
>>> id(val)
4344259184
>>> print(val)
Hello, world!
We can perform various operations on Strings, and you may have wondered why a simple replace operation returns a value instead of updating an existing one:
>>> new_val = val.replace("w", "W")
>>> id(val)
4344259184
>>> print(val)
Hello, world!
>>> id(new_val)
4344259760
>>> print(new_val)
Hello, World!
This behavior is precisely because the String data type is immutable. Any operation you perform on it will create a new String object while the original object remains unchanged.
When you assign a new value to a variable, it will not update an existing object. What is happening is that a new object is created in memory, and the variable is now pointing to a new value. Old value, if no other variables pointing to it left, can now be garbage collected:
>>> val = "Hi, world!"
>>> id(val)
4344259696
String is a sequence of characters, and it means you can access individual characters by index:
>>> val[0]
'H'
>>> val[1]
'e'
Because of this, one may think it is possible to mutate a String by assigning a value using an index (like you would with Lists, which we'll look into a little bit later):
>>> val[1] = "a"
Traceback (most recent call last):
...
TypeError: 'str' object does not support item assignment
Unfortunately, this approach won't work because String is an immutable data structure.
Integers
Integers are another standard set of values used in programming. And Integer values are also immutable.
Let's initialize an Integer value:
>>> val = 13
>>> id(val)
4342213296
When it seems like we are modifying a value...
>>> val = val + 2
>>> id(val)
4342213360
Like with String values, we get a new object created that stores a new value, and our variable points to this new object.
Floats
Floating point values behave the same way Integers do:
>>> val = 13.3
>>> id(val)
4343445072
Similarly, if we modify a variable, we get a new object created:
>>> val = val + 1.3
>>> id(val)
4343445136
Booleans
Boolean is a value that can be True
or False
. Under the hood, though, bool
is a subclass of int
. And so, False
corresponds to a value of 0
, and True
corresponds to a value of 1
.
We can check this:
>>> isinstance(True, int)
True
>>> isinstance(False, int)
True
Because bool
is a subclass of int
, it inherits its parent's immutability.
>>> val = True
>>> id(val)
4352773744
>>> val = False
>>> id(val)
4352773776
>>> val = True
>>> id(val)
4352773744
As True
and False
are constant, immutable values, when an assignment occurs, no new value is created; instead, a single value can be shared across all occurrences/usages. This behavior is one of the benefits of using immutable values.
Tuples
Tuples are ordered collections of items enclosed in parentheses (). Tuples are immutable, meaning you cannot modify, add, or remove elements from an existing Tuple. You can concatenate or slice existing Tuples, which will create a new Tuple. For example:
>>> val = (3, 4)
>>> id(val)
4344259520
>>> val = val + (5,)
>>> id(val)
4344259840
Any operation on a Tuple will create a new Tuple, such as with numbers or strings that we have already explored.
Like with strings, you can use the indexing operator to access individual items from a Tuple. You cannot use the indexing operator on the left side of an assignment because Tuples are immutable:
>>> val[0] = 2
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
Frozen sets
The built-in data type frozenset
provides an immutable version of the regular set. Once initialized, the set cannot be updated — you cannot add or remove values. frozenset
only accepts hashable values.
>>> val = frozenset([1, 2, 3, 4, 5])
>>> id(val)
4299206944
>>> val[1] = 1
Traceback (most recent call last):
...
TypeError: 'frozenset' object does not support item assignment
Conclusion on Immutable Data Types
Immutable data types are helpful in situations where you want to ensure the integrity and consistency of data. Since their values cannot be changed directly, they provide safety and reliability, especially in concurrent or multi-threaded environments. Immutable data types can be reliably used as keys in Dictionaries or as elements in Sets. Additionally, immutable objects can be shared and passed around without worrying about unintended modifications, leading to better performance and reduced memory usage in certain scenarios.
While immutable data types offer these benefits, it's important to note that not all data can or should be immutable. Mutable data types may be more appropriate in cases where data needs to be modified, such as in-place updates or performance-critical scenarios.
Mutable built-in types in Python
In Python, mutable data types are those whose values can be modified after creation. This means you can change the individual elements or items within the data structure without creating a new object. Python's three main mutable data types are Lists, Dictionaries, and Sets.
Lists
The List is a classic example of a mutable data type. Lists are sequences of arbitrary objects, just like Tuples. In a List, unlike a Tuple, you can change the value of any item using an indexing operator without changing the List's identity.
Let's take a look at an example:
>>> val = [3, 13, 173]
>>> id(val)
4343295552
>>> id(val[2])
4342406896
>>> val[1] = 27 # Mutation
>>> id(val)
4343295552
>>> id(val[1])
4342213744
>>> val
[3, 27, 173]
Note that the list's identity doesn't change when you do the mutation, but the identity of the mutated data item does change. Index 1 references a new memory position when storing a new number object.
Any other operation on the List also mutates the List and does not create a new object.
Dictionaries
A Dictionary is the only mapping type among Python's built-in data types. It allows you to store a collection of key-value pairs. Dictionaries are mutable, so you can change the value of a dictionary - its key-value pairs - without changing the identity of a Dictionary - the keys in a Dictionary work as identifiers that hold references to specific values. You can use keys to access and modify the values stored in a given Dictionary. A Dictionary key needs to be hashable.
>>> val = {"debit_card": 120, "credit_card": 1000}
>>> id(val)
4298167168
>>> id(val["debit_card"])
4297349200
>>> val["debit_card"] = 150 # Mutation
>>> id(val)
4298167168
>>> id(val["debit_card"])
4297350160
Similar to Lists, mutation changes the reference of the mutated element. But the Dictionary itself and its reference to it stay the same.
Sets
Python's Sets are a container data type. They represent an unordered container of unique hashable objects. Similar to Lists and Dictionaries, Sets are also mutable.
Two main differences stand out when comparing Sets to Lists. First, Sets don't maintain a specific order, so they cannot be accessed using indices. Second, Sets eliminate duplicate items, while Lists allow duplicates.
Sets and Dictionaries are closely related. In Python, a Set functions as a special kind of Dictionary that contains only keys instead of key-value pairs. Due to this characteristic, the items in a Set must be hashable and unique.
>>> val = {"credit_card", "debit_card"}
>>> id(val)
4299206944
>>> val.add("bank_account") # Mutation
>>> print(val)
{'credit_card', 'bank_account', 'debit_card'}
>>> id(val)
4299206944
As you can see, the order has changed. However, the id of an object did not change when we performed a mutation.
ByteArrays
The ByteArray is a sequence of integers in the 0 to 255 range. It is similar to the bytes type but allows modification of its elements.
>>> val = bytearray(b'Hello')
>>> print(val)
bytearray(b'Hello')
>>> val[0] = ord('J') # Modifying an element
>>> print(val)
bytearray(b'Jello')
Conclusion on Mutable Data Types
Mutable data types are useful when modifying the data without creating a new object. However, this mutability also means you must be careful when working with mutable objects, as unintended modifications can lead to unexpected behavior.
Conclusion
It is essential to understand the distinction between mutable and immutable data types in Python programming. Immutable types, such as Numbers, Strings, and Tuples, are unchangeable once created, ensuring data integrity and enabling safe sharing across multiple references. Immutable types are also beneficial in concurrent or multi-threaded applications. Mutable types, like Lists and Dictionaries, allow in-place modifications, offering flexibility for data manipulation but requiring careful handling to avoid unintended side effects.