Python

Types, identity, and mutability

is against ==, and why a list and a tuple behave so differently once you pass them around.

After this lesson you can

  • Explain the difference between is and ==
  • Say which built-in types are mutable and which are not
  • Predict what happens when a mutable default argument is reused

== asks whether two values are equal. is asks whether two names point at the same object in memory. They usually agree by accident for small integers and interned strings, which is exactly what makes is dangerous to reach for out of habit.

a = [1, 2, 3]
b = [1, 2, 3]
a == b   # True  — same contents
a is b   # False — two different list objects

x = 5
y = 5
x is y   # True — small ints are cached, but this is an implementation detail

is has exactly one correct everyday use: comparing against None. value is None, never value == None — None is a singleton, so is is both correct and faster, and it is the style every linter enforces.

Mutable and immutable

str, int, float, bool, and tuple are immutable — nothing can change them in place; every "modification" makes a new object. list, dict, and set are mutable — the same object can change shape without a new one being created.

s = "hello"
s.upper()        # returns "HELLO", a new string
s                 # still "hello" — s itself never changed

nums = [1, 2, 3]
nums.append(4)    # mutates nums in place
nums              # [1, 2, 3, 4] — the same list object, changed

Try it

Two names, one listpython-3.12
def run():    original = [1, 2, 3]    alias = original    copy = original[:]     alias.append(4)    copy.append(99)     return {"original": original, "alias": alias, "copy": copy}
What to look for

The mutable default argument trap

A default argument is evaluated once, when the def runs — not on every call.

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

add_item("a")   # ['a']
add_item("b")   # ['a', 'b'] — the same list, remembered from last time

Every call that does not pass into shares and mutates the exact same list, because that list was created once, at definition time, not fresh per call. The fix is the standard sentinel:

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

This is the single most common source of a "how does this function already have data in it the first time I call it" bug in Python, and it reaches beyond lists — a default {} used as a cache, a default datetime.now() frozen at import time, anything mutable in a default behaves the same way.

Try it yourself

2 visible tests · 2 hidden tests

The starter code below has the mutable-default bug from this lesson. Fix entryPoint(name, score, board=None) so it adds {"name": name, "score": score} to board and returns board — but when board is not given, it must start a fresh, empty list every call. Two separate calls that both omit board must never share one.

  • entryPoint("Nino", 90)
  • entryPoint("Ana", 75, [{"name":"Luka","score":60}])
Loading editor…

Sign up to check the hidden tests and save your progress. Sign up