DEV Community

Cover image for Mutable or Not? Why Lists Behave Like Clay but Strings Don’t
Aaron Rose
Aaron Rose

Posted on

Mutable or Not? Why Lists Behave Like Clay but Strings Don’t

Python loves a good plot twist. Few concepts deliver more surprise endings than mutability. At first, it seems like a vocabulary word you can skip past. But it shapes how your code behaves in sneaky ways: why one variable update spreads like wildfire, while another sits frozen in time.

Let’s dig into what’s really going on.


What “Mutable” Really Means

A mutable object can be changed in place. Like clay, you can reshape it without throwing it out.

  • A list is mutable. Append, extend, remove—it’s still the same list, just sculpted differently.
  • A dictionary is mutable. Add or delete keys and the object lives on.

Immutable objects are more like glass: they don’t bend. If you want something different, you have to create a whole new one.

  • Strings are immutable. You can’t replace a single character directly.
  • Tuples are immutable. Once set, their contents can’t be reassigned.

The Identity Test: Same Object or Just a Lookalike?

Python variables don’t hold objects; they reference them. Two variables may point to the same object or to separate ones. You can check this with the built-in id() function.

numbers = [1, 2, 3]
alias = numbers
alias.append(4)

print(numbers)           # [1, 2, 3, 4]
print(alias)             # [1, 2, 3, 4]
print(id(numbers) == id(alias))  # True
Enter fullscreen mode Exit fullscreen mode

Both names point to the same list object. Modify one, you modify both.

Now strings:

greeting = "hello"
copy = greeting
print(id(greeting) == id(copy))  # True (same object for now)

copy += " world"
print(greeting)   # "hello"
print(copy)       # "hello world"
print(id(greeting) == id(copy))  # False (new object created)
Enter fullscreen mode Exit fullscreen mode

The difference isn’t in assignment—both alias = numbers and copy = greeting initially point to the same object. The fork in the road comes when you change them: lists mutate in place, while strings create new objects and re-bind the variable.


Why Mutability Catches Beginners Off Guard

Here’s the trap that’s launched a thousand bug reports:

def add_item(item, bucket=[]):
    bucket.append(item)
    return bucket

print(add_item("apple"))   # ['apple']
print(add_item("banana"))  # ['apple', 'banana']  <- surprise!
Enter fullscreen mode Exit fullscreen mode

The default list isn’t reset between calls. Python creates it once and keeps reusing it. The fix is to default to None and build the list inside:

def add_item(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket
Enter fullscreen mode Exit fullscreen mode

Mutable defaults look innocent but hang around forever.


The Subtle Power of Immutability

Immutability feels restrictive at first. But it’s got perks:

  • Safety in sharing. Hand an immutable object to a function and you know it won’t come back altered.
  • Hashability. Immutable objects can live in sets or be dictionary keys. Lists can’t.
  • Predictability. Tuples and strings never change under your feet.

In short: immutability is guardrails you didn’t know you needed.


When Mutability Is Exactly What You Want

On the flip side, mutability is efficient for evolving collections:

  • Growing datasets (lists, dicts, sets)
  • Tracking state that changes over time (shopping carts, leaderboards)
  • Avoiding the overhead of constant object creation

It’s like stirring a stew: you keep adding ingredients until it’s done.


Mixed Messages: Immutable Containers with Mutable Stuff

A tuple is immutable, but it can hold mutable objects:

t = ([1, 2], [3, 4])
t[0].append(99)
print(t)  # ([1, 2, 99], [3, 4])
Enter fullscreen mode Exit fullscreen mode

The tuple itself can’t be reassigned, but the list inside it can be poked and prodded. It’s glass on the outside, clay on the inside.


How to Think About Mutability Without Losing Your Mind

Here’s a working mental model:

  • If an object can change without producing a new one, it’s mutable.
  • If any “change” gives you a new object, it’s immutable.

And here’s the golden rule: know what kind of object you’re passing around. Bugs happen when you accidentally hand out a live grenade instead of a snapshot.


Takeaways to Keep in Your Pocket

  1. Lists, dicts, and sets are mutable. Strings, tuples, and frozensets aren’t.
  2. Mutability can save time and memory, but it can also cause unexpected ripple effects.
  3. Immutability brings safety and makes objects usable as set members or dict keys.
  4. Default mutable arguments in functions are a trap—use None instead.
  5. Tuples can hide mutable insides, so don’t assume “immutable” means untouchable.

Mutability is one of those Python ideas you can’t escape. The sooner you start thinking of lists as clay and strings as glass, the less likely you are to end up sweeping up shards in your code.


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Top comments (0)