Immutable and Mutable Data Types in Python

Learn the critical difference between immutable and mutable types in Python, which types belong to each category, and how mutability affects your code.

5 min read

Every value in Python belongs to exactly one data type, and every data type falls into one of two fundamental categories: mutable and immutable. Immutable objects cannot be changed after they are created. Any operation that looks like a modification, such as replacing a character in a string or adding two integers together, actually creates a brand new object and leaves the original untouched. Mutable objects can be changed in place. Adding an item to a list, updating a value in a dictionary, or inserting an element into a set all modify the existing object without creating a new one. This distinction is not an academic curiosity. It controls which types can serve as dictionary keys, how function arguments behave, and whether changes made in one part of your program are visible in another.

If you have explored the Python data types overview and the article on Python variables and object references, you already know that Python variables are labels that point to objects in memory. The mutability of the object at the end of that label determines what happens when you perform operations on it. This article explains exactly which types are immutable, which are mutable, and the three practical consequences that affect everyday Python code: hashing and dictionary keys, function argument behavior, and the mutable default argument pitfall.

The immutable types: numbers, strings, and tuples

All of Python's numeric types are immutable. Integers, floats, complex numbers, and booleans cannot change once created. When you write x = 5 followed by x = x + 1, Python does not modify the integer 5 to become 6. It computes a new integer 6 and updates the label x to point to the new object. The original 5 still exists in memory until no labels reference it, at which point Python's garbage collector reclaims the space. You can verify this behavior using the built-in id function, which returns the memory address of an object. After the addition, the id of x will be different from what it was before, confirming that a new object was created.

Strings are also immutable. Every string method that appears to modify a string, such as upper, lower, replace, or strip, returns a new string object and leaves the original string unchanged. If you call text.upper() and do not assign the result back to a variable, the uppercased version is lost. This is why you will often see patterns like text = text.strip() in Python code. The strip method returns a new string with whitespace removed from both ends, and you must capture that return value because the original string is immutable and cannot be modified in place.

Tuples are the immutable counterpart to lists. Once you create a tuple, you cannot add items, remove items, or change the value at a particular index. Any attempt to assign to a tuple index raises a TypeError. This immutability is what makes tuples eligible to be dictionary keys, while lists are not. If you have a collection of data that should not change after creation, such as coordinates, RGB color values, or the fields of a database record, a tuple signals that intent clearly to anyone reading your code.

The mutable types: lists, dictionaries, and sets

Lists are the most commonly used mutable type. You can append items to a list, remove items with pop or remove, insert at a specific index, extend with another sequence, and reassign individual elements by index. All of these operations modify the existing list object in place. If two variables reference the same list and you modify it through one variable, the change is visible through the other variable because both are looking at the same object in memory. This aliasing behavior is the most common source of mutability-related bugs for beginners, and understanding it deeply will help you avoid the kind of issues covered in the boolean data type article where truthiness and identity can interact in unexpected ways.

Dictionaries are mutable mappings from keys to values. You can add new key-value pairs, update existing values, and delete entries after the dictionary is created. Sets are mutable collections of unique items. You can add elements, remove elements, and perform in-place set operations like intersection update and difference update. Both dictionaries and sets rely on hashing for their fast lookup performance, which is why their keys and elements must be immutable and hashable. You can use strings, integers, and tuples as dictionary keys, but you cannot use lists or other dictionaries because those mutable types do not have stable hash values.

Frozen sets provide an immutable alternative to sets. Once created, a frozen set cannot be modified, which makes it hashable and eligible to be used as a dictionary key or as an element of another set. Bytes objects are immutable sequences of bytes. Bytearrays are their mutable counterpart. The pattern recurs throughout Python: for each mutable container type, there is often an immutable version available when you need hashability or want to prevent accidental modification.

Why mutability matters for dictionary keys

Dictionaries and sets use hash tables internally to achieve near-instant lookups regardless of how many items they contain. When you insert a key into a dictionary, Python computes a hash value for that key using the built-in hash function. That hash value determines which internal bucket the key-value pair is stored in. If the key's value could change after insertion, its hash value might also change, and Python would no longer be able to find the entry in the original bucket. To prevent this scenario entirely, Python requires that dictionary keys and set elements be immutable and hashable.

Attempting to use a list as a dictionary key raises a TypeError with the message "unhashable type: list." The fix is straightforward. If you need a sequence as a dictionary key, convert the list to a tuple first. Since tuples are immutable, they are hashable and work perfectly as dictionary keys. The same rule applies to set elements. A set of tuples is valid. A set of lists is not.

How mutability affects function arguments

When you pass an argument to a Python function, the function receives a reference to the same object that the caller passed, not a copy. This is true for both immutable and mutable arguments, but the practical consequences differ dramatically between the two categories. If you pass an immutable object like an integer or a string to a function, any operation inside the function that appears to modify the argument actually creates a new object. The caller's original variable continues to reference the original object and is unaffected by whatever the function did internally.

Mutable arguments behave differently. If you pass a list to a function and that function appends an item to the list, the caller sees the change because both the caller and the function hold references to the same list object. This behavior is often desirable. Functions that sort lists in place, for example, rely on mutability to avoid making unnecessary copies. But it can also cause bugs when a function unintentionally modifies data that the caller expected to remain unchanged.

The most famous mutability-related pitfall is the mutable default argument. Python evaluates default argument values exactly once, at function definition time, not each time the function is called. If you write def add_item(item, target=[]), the empty list is created once when Python first reads the function definition. Every call that omits the target argument shares that same list object. After the first call appends an item, the second call finds the list already populated, which is almost never what the programmer intended. The standard fix is to use None as the default and create the mutable object inside the function body when the argument is None.

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

This pattern appears in virtually every Python codebase. The default value None is immutable and safe. The function checks for None on each call and creates a fresh list only when needed. Each caller gets its own independent list, and the unexpected sharing that plagues mutable defaults is eliminated.

Rune AI

Rune AI

Key Insights

  • Immutable types cannot change after creation; mutable types can be modified in place.
  • Numbers, strings, tuples, and frozensets are immutable; lists, dicts, and sets are mutable.
  • Only immutable, hashable types can serve as dictionary keys or set elements.
  • Passing a mutable object to a function allows the function to modify the original.
  • Never use a mutable object as a default argument value in a function definition.
RunePowered by Rune AI

Frequently Asked Questions

Which Python types are immutable?

Immutable types include integers, floats, complex numbers, booleans, strings, tuples, frozensets, bytes, and None. Once an object of these types is created, its value cannot change. Any operation that appears to modify an immutable object actually creates a new object. For example, concatenating two strings produces a new string rather than modifying either original.

Which Python types are mutable?

Mutable types include lists, dictionaries, sets, and bytearrays. You can change the contents of these objects after they are created. Adding an item to a list, updating a dictionary value, or adding to a set all modify the existing object rather than creating a new one. Custom classes are mutable by default unless designed otherwise.

Why does mutability matter in Python?

Mutability affects three main areas. First, only immutable objects can be used as dictionary keys or set elements because Python needs a stable hash value. Second, mutable default arguments in functions can cause unexpected behavior because the same object is shared across calls. Third, when you pass a mutable object to a function, changes made inside the function are visible outside because both the caller and the function hold a reference to the same object.

Conclusion

The split between immutable and mutable types is not an implementation detail. It determines which types can be dictionary keys, how function arguments behave, and whether your data is safe from unexpected modification. Learning this distinction early prevents a whole category of bugs that trip up new Python programmers.