Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Python

Python vs C++ Series: Mutable, Immutable, and Copy Assignment

4.85/5 (9 votes)
25 Oct 2021CPOL7 min read 11.5K  
Mutable, Immutable and copy assignment in Python vs. C++
Python objects’ immutability is defined by their types. Knowing which data types are mutable, which are not and the behavior when using mutable objects in certain situations is critical to avoid writing bug code.

The third article of the Python vs C++ Series is about immutability – an object cannot be modified after it is created.

(Note that the Python code in the series assumes Python 3.7 or newer.)

Const and Constexpr in C++

C++ supports two notions of immutability: const and constexpr. To declare an object is immutable, we use either const or constexpr when defining an object. Of course, there are more details than this when we consider the immutability in C++, but in general, all objects are mutable by default.

C++
void main()
{
    int nonConstVariable = 0; // Non-const object
    const int constVariable = 0; // Const object
    constexpr int secondsPerHour = 60 * 60; // Const expression
}

This article uses the word mutable in general terms, so don’t be confused with the C++ mutable keyword, which allows a class member to be changeable even if its class instance is const or allows a class member modified by a const method.

C++
class MyClass
{
    public:
        int variable1 = 0;
        mutable int variable2 = 0;
};

void main()
{
    const MyClass myClass; // Const object
    myClass.variable2 = 10; // Ok because variable2 is mutable
    myClass.variable1 = 10; // Error; myClass object is const
}

Mutability and Immutability in Python

Unlike C++, where every object is mutable by default, Python objects’ immutability is determined by their type. The list below summarises some common mutable and immutable data types.

(See Built-in Types for more detail.)

What Does Immutable Mean in Python?

When we are new to Python, we might think everything is mutable in Python because we can update whatever objects we want. For example, the following code will work without an issue.

Python
variable = 10
variable = "string"
variable = 2.0
variable = [1, 2, 3]

However, the meaning of updating objects is different between immutable objects and mutable objects. When assigning an object to a variable, we can think that the variable is a named pointer pointing to the object (we will talk more about this in the Copy Assignment section). If the object is immutable when we update the variable, we actually point it to another object, and Python’s garbage collection will recycle the original object if it is no longer used. On the contrary, if a variable points to a mutable object, the mutable object will be modified when we update the variable.

We can use a built-in function id to verify if an object we updated is still the same object. The id function returns the identity (the object’s address in memory) of an object. The following example shows how immutability behaves in Python and how we use the id function to check objects’ identities. Also, we use the hex function to convert the id output to hexadecimal format, so it looks more like a memory address.

Python
# int is immutable type
integer = 10
print(f"integer: {integer}; address: {hex(id(integer))}")
# integer: 10; address: 0x7f7a7b35fa50
integer = 20
print(f"integer: {integer}; address: {hex(id(integer))}")
# integer: 20; address: 0x7f7a7b35fb90

# str is immutable type
string = "hello"
print(f"string: {string}; address: {hex(id(string))}")
# string: hello; address: 0x7f7a7b205370
string = "world"
print(f"string: {string}; address: {hex(id(string))}")
# string: world; address: 0x7f7a7b205470

# list is mutable type
list_var = [1, 2, 3]
print(f"list_var: {list_var}; address: {hex(id(list_var))}")
# list_var: [1, 2, 3]; address: 0x7f7a7b259840
list_var.append(4)
print(f"list_var: {list_var}; address: {hex(id(list_var))}")
# list_var: [1, 2, 3, 4]; address: 0x7f7a7b259840

# dictionary is mutable type
dict_var = {"key1": "value1"}
print(f"dict_var: {dict_var}; address: {hex(id(dict_var))}")
# dict_var: {'key1': 'value1'}; address: 0x7f7a7b2cf500
dict_var["key2"] = "value2"
print(f"dict_var: {dict_var}; address: {hex(id(dict_var))}")
# dict_var: {'key1': 'value1', 'key2': 'value2'}; address: 0x7f7a7b2cf500

We can see that the address of variables integer and string is different before and after updating them, which means these two variables point to new objects (20 and world respectively) after updating. On the contrary, list_var and dict_var are mutable, so their addresses remain the same before and after updating them. Therefore, they still point to the same objects.

Why Knowing Python Objects’ Mutability and Immutability Is Essential?

That’s because if we are blind to Python objects’ immutability, we may be surprised by their behavior, and sometimes working with mutable objects without care leads to bugs. The following subsections will discuss a few scenarios that may not be intuitive for people from C++ backgrounds.

Use Mutable Value as Default Function Parameters

Python’s default argument feature allows us to provide default values for function arguments when defining a function. However, if our default values are mutable type, their behavior may not be desired. See an example below.

Python
# Use an empty list as the default value.
def my_function_1(parameter: List = []) -> None:
    parameter.append(10)
    print(parameter)

In this example, we define a function (my_function_1) and use an empty list as the default value. And then we try to call this function without providing a parameter several times.

Python
my_function_1()
# [10]
my_function_1()
# [10, 10]
my_function_1()
# [10, 10, 10]

If we run the code, we will notice that the parameter keeps the value (i.e., 10) we append every time. Therefore, in the third time, the output becomes [10, 10, 10]. The behavior is actually similar to that we define a static variable in a function in C++ – the static variable is initialized only one time and holds the value even through function calls.

How to Avoid This Situation?

If using mutable type, such as list, is necessary (which is common), we should use None as the default value and Optional for type checking (Using Optional tells type checkers the value could be either None or the desired type). This way guarantees the parameter is new whenever we call the function without providing a parameter. See an example below.

Python
# Use None as the default value. And use Optional for type checking
def my_function_2(parameter: Optional[List] = None) -> None:
    if parameter:
        parameter.append(10)
    else:
        parameter = [10]
    print(parameter)

This time, if we call the function (my_function_2) without providing a parameter several times, the parameter will not hold the value from the previous call.

Python
my_function_2()
# [10]
my_function_2()
# [10]

In short, do not use mutable type as the default value of a function parameter. If the parameter needs to be mutable type, use None as the default value.

The Behavior of Class Variable

The second scenario happens when we use a mutable variable as a class variable. A class variable in Python is shared by all instances. The scenario is similar to the C++ class variable (i.e., declare a class variable with static). To define a class variable in Python, define a variable inside the class but outside any method. In the example below, we define a class (MyClass) with a mutable variable (mutable_member), an immutable variable (immutable_member), and a couple of instance variables.

Python
from typing import List

class MyClass:
    # Mutable class variable. Shared by all instances
    mutable_member: List = []

    # Immutable class variable.
    # Shared by all instances unless an instance binds
    # this variable to something else.
    immutable_member: int = 0

    def __init__(self) -> None:
        # Instance variables are unique to each instance.
        self.immutable_instance_variable: int = 0
        self.mutable_instance_variable: List = []

Since class variable is shared by all instances, if the variable is a mutable type, the change of the variable will affect all instances of the class. Before we update the class variables,  let’s check their value and memory address.

Python
print(f"MyClass.mutable_member: {MyClass.mutable_member}")
# MyClass.mutable_member: []
print(f"MyClass.mutable_member address: {hex(id(MyClass.mutable_member))}")
# MyClass.mutable_member address: 0x7f0f7092fe40
print(f"MyClass.immutable_member address: {hex(id(MyClass.immutable_member))}")
# MyClass.immutable_member address: 0x7f0f70b34910

class1 = MyClass()
print(f"class1.mutable_member: {class1.mutable_member}")
# class1.mutable_member: []
print(f"class1.mutable_member address: {hex(id(class1.mutable_member))}")
# class1.mutable_member address: 0x7f0f7092fe40
print(f"class1.immutable_member address: {hex(id(class1.immutable_member))}")
# class1.immutable_member address: 0x7f0f70b34910

class2 = MyClass()
print(f"class2.mutable_member: {class2.mutable_member}")
# class2.mutable_member: []
print(f"class2.mutable_member address: {hex(id(class2.mutable_member))}")
# class2.mutable_member address: 0x7f0f7092fe40
print(f"class2.immutable_member address: {hex(id(class2.immutable_member))}")
# class2.immutable_member address: 0x7f0f70b34910

Here, we can see both mutable_member and immutable_member of class1 and class2 point to the same objects, which are the same as MyClass.

Now, let’s update the mutable class variable, and print out their values.

Python
# Update the mutable class variable
class1.mutable_member.append(10)
print(f"class1.mutable_member: {class1.mutable_member}")
# class1.mutable_member: [10]
print(f"class2.mutable_member: {class2.mutable_member}")
# class2.mutable_member: [10]

The update affects all instances of MyClass.

How about the immutable class variable?

The behavior is a little bit different when we update an immutable class variable. Now, let’s update the immutable_member from class1, and print out the address of both instances class1 and class2.

Python
# Update the immutable class variable
class1.immutable_member = 20
print(f"class1.immutable_member: {class1.immutable_member}")
# class1.immutable_member: 20
print(f"class1.immutable_member address: {hex(id(class1.immutable_member))}")
# class1.immutable_member address: 0x7f0f70b34b90
print(f"class2.immutable_member: {class2.immutable_member}")
# class2.immutable_member: 0
print(f"class2.immutable_member address: {hex(id(class2.immutable_member))}")
# class2.immutable_member address: 0x7f0f70b34910

The output shows that class1.immutable_member no longer binds to the MyClass.immutable_member.

If we create a new MyClass instance class3, its class variables are still bound to MyClass’ class variables.

Python
class3 = MyClass()
print(f"class3.immutable_member: {class3.immutable_member}")
# class3.immutable_member: 0
print(f"class3.immutable_member address: {hex(id(class3.immutable_member))}")
# class3.immutable_member address: 0x7f0f70b34910
print(f"MyClass.immutable_member address: {hex(id(MyClass.immutable_member))}")
# MyClass.immutable_member address: 0x7f0f70b34910

Hence, class variables are shared by all instances. For a mutable class variable, if we modify it, the change will affect all instances, whereas, for an immutable class variable, if we change it from a class instance, the variable of this instance no longer binds to the original class variable.

Also, it is worth mentioning that instance variables are unique to each class instance regardless of its mutability.

Python
# Instance variables are unique to each instance
class1.mutable_instance_variable.append(30)
print(f"class1.mutable_instance_variable: {class1.mutable_instance_variable}")
# class1.mutable_instance_variable: [30]
print(
    f"class1.mutable_instance_variable address: "
    f"{hex(id(class1.mutable_instance_variable))}"
)
# class1.mutable_instance_variable address: 0x7f0f709e6140
print(f"class2.mutable_instance_variable: {class2.mutable_instance_variable}")
# class2.mutable_instance_variable: []
print(
    f"class2.mutable_instance_variable address: "
    f"{hex(id(class2.mutable_instance_variable))}"
)
# class2.mutable_instance_variable address: 0x7f0f709e6180

Copy Assignment

Using the assignment operator (e.g., x = 10) in Python does not create copies of objects. Instead, it establishes a binding between the variables and the objects. The behavior is not a problem when working with immutable objects. The assignment operator will create a new binding between the variable and the new target for immutable objects. We can verify it using the is operator to check if two objects are the same object (we can also use the id function as we did in the previous examples). The following example shows the assignment operation behavior on immutable objects and the usage of the is operator.

Python
# Copy an immutable object
my_string = "hello"
my_string_copy = my_string
# They both bind to the same object.
print(my_string is my_string_copy)
# True

# After we update one variable, they bind to two different objects.
my_string_copy += " world"
print(my_string is my_string_copy)
# False

# Of course, their values are different.
print(f"my_string: {my_string}")
# my_string: hello
print(f"my_string_copy: {my_string_copy}")
# my_string_copy: hello world

Copy Module and Shallow Copy

We know copy assignment only creates a binding between the object and the target, so how do we create a copy of an object? Python provides a copy module that offers shallow and deep copy operations. The following example uses copy.copy function to create a copy (value_copy) from a mutable object (value).

Python
import copy

# Define a mutable object.
value = [1, 2, 3]
print(hex(id(value)))
# 0x7fc0df486380

# Copy assignment just creates binding.
value_bind = value
print(hex(id(value_bind)))
# 0x7fc0df486380

# Use copy.copy function to perform shallow copy.
value_copy = copy.copy(value)
print(hex(id(value_copy)))
# 0x7fc0df4af740

# Update the copied variable.
value_copy.append(4)
print(value_copy)
# [1, 2, 3, 4]

# The update does not affect the original variable.
print(value)
# [1, 2, 3]

From this example, the value_copy variable is independent of the value variable. Also, worth mentioning that the copy.copy function still creates a binding between the object and the target for an immutable object.

Shallow copy on mutable compound objects or immutable compound objects contains mutable compound objects

Shallow copy happens in C++ when coping with a compound object (e.g., a class) containing pointers; the copy copies the pointers but not the objects the pointers point to (See the diagram below).

Image 1

Therefore, the object that the pointers of Object A and Object B point to become shared.

Since the copy.copy function in Python performs shallow copy, the situation mentioned above happens when copying a compound object with mutable compound objects, whether the top-level compound object is mutable or immutable. The following example demonstrates the shallow copy scenario.

Python
import copy

# Create an object with an mutable object,
# and print out its memory address.
compound_object = {"key1": 123, "key2": [1, 2, 3]}
print(hex(id(compound_object)))
# 0x7fbd2b61d480

# Use copy.copy to create a copy of the compound_object.
# and print out its memory address.
compound_object_copy = copy.copy(compound_object)
print(hex(id(compound_object_copy)))
# 0x7fbd2b61d540

# The address shows compound_object_copy is a different object
# from compound_object.
# However, if we print out the address of the key2 value
# from both compound_object and compound_object_copy,
# they are the same object.
print(hex(id(compound_object["key2"])))
# 0x7fbd2b5561c0
print(hex(id(compound_object_copy["key2"])))
# 0x7fbd2b5561c0

# Since key2 is shared, if we update it, the change will
# affect both compound_object and compound_object_copy.
compound_object_copy["key2"].append(4)
print(compound_object_copy)
# {'key1': 123, 'key2': [1, 2, 3, 4]}
print(compound_object)
# {'key1': 123, 'key2': [1, 2, 3, 4]}

Use copy.deepcopy function to perform a deep copy

If we want to perform deep copy, we should use copy.deepcopy instead. See the following example.

Python
import copy

# Create an object with an mutable object,
# and print out its memory address.
compound_object = {"key1": 123, "key2": [1, 2, 3]}
print(hex(id(compound_object)))
# 0x7fbba9d11480

# Use copy.deepcopy to create a copy of the compound_object.
# and print out its memory address.
compound_object_copy = copy.deepcopy(compound_object)
print(hex(id(compound_object_copy)))
# 0x7fbba9c3b600

# Also print out the address of the key2 value from both
# compound_object and compound_object_copy, and they are
# different objects.
print(hex(id(compound_object["key2"])))
# 0x7fbba9c4a180
print(hex(id(compound_object_copy["key2"])))
# 0x7fbba9c6d9c0

# Therefore, if we update the key2 value from compound_object_copy,
# it does not affect compound_object.
compound_object_copy["key2"].append(4)
print(compound_object_copy)
# {'key1': 123, 'key2': [1, 2, 3, 4]}
print(compound_object)
# {'key1': 123, 'key2': [1, 2, 3]}

Conclusion

Python objects’ immutability is defined by their types. Knowing which data types are mutable, which are not and the behavior when using mutable objects in certain situations is critical to avoid writing bug code.

(All the example code is also available at mutable_immutable_and_copy_assignment.)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)