Mutability, copies and comprehensions
Why two names can point at one list, the mutable default argument trap, and how to write comprehensions that stay readable.
A large share of Python screening questions are really about one idea: some objects can be changed in place and others cannot. Once you can tell which is which, questions about copies, default arguments and comprehensions become straightforward.
Names, objects and copies
Lists, dicts and sets are mutable. Strings, numbers and tuples are not. Assigning a list to a second name does not copy it; both names point at the same object.
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4]
c = a.copy() # or list(a) or a[:]
c.append(5)
print(a) # [1, 2, 3, 4]
print(c) # [1, 2, 3, 4, 5]copy() makes a shallow copy: the outer list is new, but any lists inside it are still shared. Use copy.deepcopy() when the structure is nested and you need full independence. Use is to ask whether two names refer to the same object and == to ask whether their contents match. For None, always write x is None.
The mutable default argument
Default values are created once, when the function is defined, not on each call. A mutable default therefore remembers changes between calls.
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] <- surprise
def add_item_safe(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item_safe("b")) # ['b']The None pattern is the accepted fix and is a frequent interview question in its own right.
Comprehensions
A comprehension builds a list, dict or set in one expression. Read it left to right: what to produce, where it comes from, and an optional filter.
squares = [n * n for n in range(5)]
print(squares) # [0, 1, 4, 9, 16]
evens = [n for n in range(10) if n % 2 == 0]
print(evens) # [0, 2, 4, 6, 8]
lengths = {w: len(w) for w in ["pen", "table"]}
print(lengths) # {'pen': 3, 'table': 5}A comprehension creates a new object, so it never changes the source. Keep it to one filter and one expression; if you need nested conditions or several steps, a plain loop is clearer and reviewers prefer it.
Common mistakes and a practice habit
- Expecting
b = ato protectafrom changes made throughb. - Writing
if x == Noneinstead ofif x is None. - Using
sorted(items)anditems.sort()interchangeably. The first returns a new list; the second sorts in place and returnsNone. - Using a list or dict as a default argument.
Practice habit: Whenever you write a function that takes a list or dict, ask yourself: do I change the caller's object or return a new one? Say the answer out loud, then add a one-line docstring that states it. This habit makes you quick at the "what does this print" questions and makes your code easier for a team to trust.
More in Programming