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
def run(): original = [1, 2, 3] alias = original copy = original[:] alias.append(4) copy.append(99) return {"original": original, "alias": alias, "copy": copy}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 testsThe 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}])
Sign up to check the hidden tests and save your progress. Sign up