GoByte Skills Episode 9 of 27, track Python (3 of 3)

Mutable defaults: evaluated once

GoByte Skills #9: A default argument is evaluated when def runs, not on each call, and stored on the function. So a list default is one list that every call shares. Why, the None fix, and why += is the same bug in disguise.

Zoe is one of GoByte's characters. This post was drafted by AI agents in Zoe's voice, then fact checked, run and edited by the GoByte team.

Back to top

A default argument is evaluated when the function is defined, not each call, so a list default is shared.

Animation for GoByte Skills #9: One list is created at def time and every call appends to the same one.
Transcript

One list is created when def runs and is stored on the function; each call that omits the argument points at that same list and appends to it, so the list keeps growing across calls.

def add(item, bag=[]):
    bag.append(item)
    return bag

print(add(1))
print(add(2))
print(add.__defaults__)

It prints:

[1]
[1, 2]
([1, 2],)

Why#

def is a statement, and at module level it runs once. (A def nested in another function runs on every outer call, so each call gets a fresh default.) It evaluates each default expression right then and stores the results on the function object, in add.__defaults__. Every call that omits bag binds that same stored list. append mutates it, so the second call finds the first call's leftovers. Nothing is remembered by magic: the object simply lives as long as the function does.

The fix is a sentinel. The body runs on every call, so the list is new each time:

def add(item, bag=None):
    if bag is None:
        bag = []
    bag.append(item)
    return bag

The edges#

  • It is about mutation, not lists. bag = bag + [item] builds a new list and is safe. bag += [item] mutates in place: the same bug in a disguise.
  • Every default is frozen at def time. def stamp(t=time.time()) returns the same timestamp forever.
  • dataclasses refuses the trap: items: list = [] raises ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory.
  • Ruff flags it as B006, so let a linter catch it before a reviewer does.
  • The rare deliberate use is a cache shared across calls. If you mean that, say so in a comment.

Rule of thumb#

Defaults should be immutable: None, numbers, strings, tuples whose items are immutable too (a tuple holding a list shares that list). A list default is the office fridge: shared by everyone, labeled by no one, and something from last Tuesday is still in it.

Report a mistake

Your product here? Partner with us

Back to top

Discussion

No comments yet. Signed in GoByte members with a verified e-mail can join. Community guidelines

Reading is open to everyone. Commenting and voting need a GoByte account with a verified e-mail.