Back to Blog
Python

Python Mutable vs Immutable: How It Impacts Your Code

python mutable vs immutable: Understand how Python's mutable and immutable types behave differently in assignment, function calls, and memory usage, and how to choose...

PythonMutableImmutableData TypesObject References
Illustration comparing mutable and immutable objects in Python, showing a list that can be modified and a tuple that cannot.

python mutable vs immutable requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Python's distinction between mutable and immutable objects is not a language trivia fact. It determines whether an assignment creates a new object or modifies an existing one, which in turn affects function arguments, data sharing, and even performance. Consider this simple example:

a = [1, 2, 3] b = a b.append(4) print(a) # [1, 2, 3, 4]

Now compare that with a tuple:

a = (1, 2, 3) b = a # b[0] = 99 # TypeError: 'tuple' object does not support item assignment

The list changed because lists are mutable. The tuple could not change because tuples are immutable. This core difference influences how you write, debug, and optimize Python code.

What Makes an Object Mutable or Immutable

In Python, every object has a type, an identity, and a value. Mutability refers to whether the value can change after the object is created. If the value can be altered in place, the object is mutable. If the value is fixed at creation time, the object is immutable.

The distinction is enforced at the language level. For immutable types, any operation that appears to modify the object actually creates a new object and rebinds the name. For mutable types, operations like append, update, or __setitem__ change the object's internal state without changing its identity.

This behavior is not just an implementation detail. It is part of Python's object model and has direct consequences for how data flows through your program.

Mutable Types in Python

The most common mutable types are list, dict, set, and bytearray. These types provide methods that modify the object in place:

items = [1, 2, 3] items.append(4) # list modified in place config = {"host": "localhost"} config["port"] = 8080 # dict modified in place unique = {1, 2, 3} unique.add(4) # set modified in place

When you pass a mutable object to a function, the function receives a reference to the same object. Changes made inside the function are visible outside it, unless the function explicitly creates a copy.

def add_item(lst): lst.append(99) my_list = [1, 2] add_item(my_list) print(my_list) # [1, 2, 99]

This behavior is useful when you want to share state, but it also introduces the risk of unintended side effects.

Immutable Types in Python

Immutable types include int, float, str, tuple, frozenset, and bytes. Once created, their value cannot be changed. Operations that look like modification return a new object instead:

name = "python" new_name = name.upper() # creates a new string print(name) # "python" print(new_name) # "PYTHON" point = (1, 2) # point[0] = 3 # TypeError

Even though integers are immutable, you can reassign a variable to a different integer. That does not change the original integer object; it rebinds the name to a new object. The old object becomes eligible for garbage collection if no other references exist.

Immutability provides a guarantee that the value will not change unexpectedly. This makes immutable objects safe to share across threads, use as dictionary keys, and store in sets, because their hash value remains stable.

How Assignment and References Work

When you write b = a, you are not copying the object. You are copying the reference. Both names point to the same object in memory. For mutable objects, this means changes through one name affect the other. For immutable objects, since the value cannot change, this aliasing is harmless.

a = [1, 2, 3] b = a print(a is b) # True c = (1, 2, 3) d = c print(c is d) # True

The is operator checks identity, not value. For immutable objects, Python sometimes reuses existing objects (like small integers), but that is an implementation optimization and not something you should rely on for correctness.

Understanding references is critical when you need to copy a mutable object. A shallow copy using copy.copy or list.copy() creates a new container but shares the nested mutable elements. A deep copy with copy.deepcopy recursively copies everything. Choosing the right copy method depends on whether you need to isolate the entire structure or just the top level.

Implications for Function Arguments and Defaults

The mutability of an object directly affects how it behaves as a function argument. If you pass a mutable object and modify it inside the function, the caller sees the change. This can be intentional, but it often leads to bugs when the function is not expected to mutate its input.

A classic pitfall is using a mutable default argument:

def add_item(item, items=[]): items.append(item) return items print(add_item(1)) # [1] print(add_item(2)) # [1, 2] # unexpected!

The default list is created once when the function is defined, not each time the function is called. Because lists are mutable, subsequent calls reuse the same list. The fix is to use None as the default and create a new list inside the function:

def add_item(item, items=None): if items is None: items = [] items.append(item) return items

Immutable defaults do not have this problem because they cannot be modified in place. A tuple or string default will remain the same across calls.

Performance and Memory Considerations

Mutability has measurable performance and memory implications. Mutable objects can be modified in place, avoiding the overhead of creating a new object each time. For example, appending to a list is amortized O(1), while concatenating strings repeatedly creates a new string each time, leading to O(n^2) behavior in a loop.

# Inefficient string building s = "" for i in range(1000): s += str(i) # Better: use a list and join parts = [] for i in range(1000): parts.append(str(i)) s = "".join(parts)

Immutable objects, however, are easier to cache and share. Because they cannot change, Python can safely reuse the same object across multiple references. This reduces memory usage when the same value appears many times, such as interned strings or small integers.

Immutable objects also have a stable hash, which makes them suitable as dictionary keys. Mutable objects like lists cannot be used as keys because their hash would change if the value changed, breaking the dictionary's invariants.

Choosing Between Mutable and Immutable Types

The choice between mutable and immutable types should be driven by how the data is used. Use a mutable type when you need to build or modify a collection incrementally, or when you want to share state across functions. Use an immutable type when the data should remain constant after creation, or when you need to use it as a dictionary key or set element.

Tuples are often used for fixed records, such as coordinates or database rows. Lists are used for dynamic sequences. frozenset provides an immutable set for cases where you need set operations but want to guarantee the contents never change.

In data-heavy applications, immutable structures can simplify reasoning about concurrency because they are inherently thread-safe. However, they may incur higher allocation overhead if you need to create many modified copies. The right choice depends on the specific pattern: if you frequently append to a collection, a list is more efficient; if you need to pass data around without worrying about accidental modification, a tuple or a custom immutable wrapper is safer.

Common Pitfalls and How to Avoid Them

One common mistake is assuming that += on a tuple or string modifies the original. It does not; it creates a new object and rebinds the name. This can be surprising when you expect in-place behavior.

Another pitfall is modifying a list while iterating over it. Because the list is mutable, removing or adding items during iteration can cause elements to be skipped or raise RuntimeError. A safer approach is to iterate over a copy or build a new list.

# Risky: modifying list during iteration numbers = [1, 2, 3, 4] for n in numbers: if n % 2 == 0: numbers.remove(n) # Safer: build a new list numbers = [n for n in numbers if n % 2 != 0]

Finally, be careful when storing mutable objects in a set or as dictionary keys. Even if you use a custom class that is technically hashable, if the object is mutable and its hash changes, the set or dictionary will behave incorrectly. Prefer immutable types for keys, or ensure your mutable objects do not change their hash after insertion.

Understanding the mutable vs immutable distinction is not just about knowing which types are which. It is about predicting how your data will behave when shared, copied, or passed around. By choosing the right kind of object for each situation, you avoid subtle bugs and write code that is both efficient and maintainable.

python mutable vs immutable: Practical Usage and Code Exampl | RYUSLOG DEV