skip to content

Which of Python's built-in types are immutable, and which can be changed in place?

level: juniorimportance: must knowfreq 78%

answer

  1. Two families of built-in types
  2. Does the object change, or the name?
  3. str methods hand back a new object
  4. list, dict, set, bytearray change in place
  5. tuple and frozenset are the frozen ones

basics

~20 s

int, float, bool, str, bytes, tuple and frozenset are immutable: every apparent change builds a new object. list, dict, set and bytearray are mutable and can be modified in place through any name that refers to them.

solid answer

~40 s

Python's built-ins split into two families. **Immutable**: `int`, `float`, `bool`, `complex`, `str`, `bytes`, `tuple`, `frozenset`, `range` and `None`. **Mutable**: `list`, `dict`, `set` and `bytearray`. Immutable means the object's value is fixed for its whole lifetime, so `s.upper()` or `s.replace(...)` returns a *new* string and leaves the original untouched; you only see a change if you rebind the name. Mutable objects are edited through the object itself, so `lst.append(x)` is visible through every name that refers to that same list. Two consequences drive most interview follow-ups: only immutable-by-construction objects are usefully hashable, so only they make good dict keys and set members; and immutability in Python is *shallow* — a tuple fixes which objects it holds, not what those objects contain, so a tuple can hold a list that still grows.

code

python · 10 lines
python
s = "bid"
print(s.upper(), s)
nums = [1, 2, 3]
nums.append(4)
print(nums)
t = (1, 2, 3)
try:
    t[0] = 9
except TypeError as exc:
    print("tuple:", exc)

go deeper

for a junior

Be ready to sort the built-ins into the two families on the spot, and to say that s.upper() hands back a new string rather than editing s. This is a warm-up question; a confident, specific list is what the interviewer wants.

for a middle

Explain the mechanics rather than the list: an immutable object's value is fixed for its lifetime, so an apparent change allocates a new object and rebinds the name. Say why frozenset and bytes exist alongside set and bytearray.

for a senior

Show where the distinction costs real money: aliasing across module boundaries, mutable module-level configuration that any importer can edit, and string building in a hot loop where repeated concatenation should be str.join.

for a principal

Own the convention for the codebase: which of your own value objects are frozen, when a read-only view such as types.MappingProxyType beats defensive copying, and how you stop mutable state from leaking across module or service boundaries.

## The distinction An object is **immutable** when its value cannot change after construction. An object is **mutable** when operations exist that change it in place, leaving the same object identity behind. The word applies to *objects*, never to names: a name is only a binding, and rebinding a name that points at an immutable object is always allowed. | Immutable built-ins | Mutable built-ins | |---|---| | `int`, `float`, `complex`, `bool` | `list` | | `str`, `bytes`, `range` | `dict` | | `tuple`, `frozenset` | `set` | | `NoneType`, `type(...)` function objects' code | `bytearray` | `frozenset` exists precisely so there is an immutable counterpart to `set`; `bytes` is the immutable counterpart to `bytearray`; `tuple` is the immutable counterpart to `list`. ## Immutable does not mean constant The most common beginner slip is reading immutability as "this name can never change". It cannot mean that, because assignment rebinds names freely: ```python s = "bid" s = s.upper() # legal: s now points at a different str object ``` Nothing was mutated. `str.upper` built a new string and the assignment pointed `s` at it. The original `"bid"` object is unchanged and, if nothing else refers to it, is simply collected. This is why every `str` method that looks like an edit — `replace`, `strip`, `lower`, `join`, `format` — *returns* a value that you must capture. Code that calls `text.strip()` and then keeps using `text` is one of the most frequent real bugs at this level. A mutable object behaves the opposite way: ```python bids = [120, 340] same = bids bids.append(500) print(same) # [120, 340, 500] - one object, two names ``` This aliasing is the source of most "why did my other variable change?" questions. Nothing was copied; two names simply refer to one list. ## Immutability is shallow A tuple freezes *which* objects it holds, not the contents of those objects: ```python row = ("campaign-7", [120, 340]) row[1].append(500) # fine - the list inside is still mutable row[1] = [] # TypeError - the tuple's own slots are fixed ``` So "a tuple is a read-only list" is only half true. The tuple's slots are read-only; the objects in those slots keep whatever mutability they had. The same holds for a `frozenset`, which sidesteps the issue by only accepting hashable members in the first place. ## Why the distinction matters in practice **Sharing.** An immutable object can be shared across threads, cached, or handed to callers with no defensive copy, because no caller can change it under you. A module-level `TIMEOUTS = {"bid": 0.2}` is a dict any importer can edit; the same data as a tuple of pairs, or wrapped in a read-only view via `types.MappingProxyType`, cannot be edited by accident. **Hashability.** The interpreter refuses to hash `list`, `dict` and `set`, so they cannot be dict keys or set members. `tuple`, `frozenset`, `str`, `bytes` and the numeric types can be. That is not an arbitrary rule: a key whose value could change would be filed under one hash and then looked up under another. **Function defaults.** Because a default value is created once, when the `def` statement runs, a mutable default is shared by every call that omits the argument — the single most-asked Python trap there is. An immutable default such as `0`, `""`, `()` or `None` has no such problem. **Building strings.** Since every concatenation of `str` allocates a new object, growing a string in a loop with `+=` does quadratic work. Collect the pieces in a list and finish with `"".join(parts)`, or accumulate into a `bytearray` when you are working with bytes. ## Checking it at the prompt There is no `is_immutable()` builtin. In practice you either know the type or you try the thing: attempting item assignment on a `tuple` or `str` raises `TypeError`, and `hash(obj)` raising `TypeError` is a strong signal that the object is mutable. Your own classes are mutable by default; freezing them means writing an immutable value type — for example a `dataclasses.dataclass(frozen=True)` or a `collections.namedtuple` — rather than relying on convention.

  • If tuples are immutable, how can the contents of a tuple still change?
    Immutability in Python is shallow. A tuple fixes which objects sit in its slots; it says nothing about those objects. `row = ("c7", [120])` will not let you replace `row[1]`, but `row[1].append(340)` works, because the list itself is still mutable. That is also why such a tuple cannot be hashed.
  • Is `bytes` mutable, and what do you use when you need to build up binary data?
    `bytes` is immutable, the binary counterpart of `str`. When you need to accumulate or patch binary data in place, use `bytearray`, which supports item assignment, `extend` and slice assignment. Convert with `bytes(buf)` when you want an immutable, hashable result to hand out.
  • How do you make an object of your own class immutable?
    There is no keyword for it. The practical options are a `collections.namedtuple` or a `dataclasses.dataclass(frozen=True)`, both of which reject attribute assignment after construction and give you a value type that hashes on its fields. Hand-rolling it means overriding `__setattr__`, which is rarely worth the noise.

An immutable object is a printed receipt: to correct it you print a new one. A mutable object is a whiteboard: everyone looking at it sees the edit you just made.

saying these in an interview costs you the question

  • Says str.replace edits the string in place
  • Claims immutable means the name cannot be rebound
  • Calls tuple deeply immutable, so its contents can never change
  • Thinks frozenset is mutable because set is
  • Believes int is mutable because a counter can be incremented
  • Assumes assigning one list to another name copies it

context