Recent chats
ASKGEEZ Q&A GUIDETechnology & devices

Why is a shared empty list a risky default for a Python function?

Quick answer

A list used as a Python function’s default is created when the function is defined, not anew for every call. If the function changes that list, a later call that omits the argument can see the earlier change. Use None as the default, then create a new list inside the function when the argument is None. [S1]

LET’S WORK THROUGH IT

The question, the details, the next step.

Question

Why is items=[] in a function definition risky? I thought the function would start with an empty list each time I call it.

Geez

The default list is made when Python defines the function. Calls that leave out items reuse that same list; they do not each get a newly made empty one. If the function appends to it, a later call that also leaves out items can see what was appended earlier. [S1]

Here is a small illustrative example:

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

print(add_item("tea"))
print(add_item("bread"))

The second call can return ['tea', 'bread'], rather than starting with only ['bread'], because both calls use the shared default list. This matters whenever a function mutates a default object, for example by appending or removing list entries. [S1]

Follow-up question

What should I write instead if I want each call without a list argument to start fresh?

Geez

Use None to signal that no list was supplied, and make the list inside the function. That gives each call with an omitted argument its own list:

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

print(add_item("tea"))    # ['tea']
print(add_item("bread"))  # ['bread']

This is the pattern recommended in the Python tutorial for a mutable default such as a list. [S1] The check is specifically is None, rather than a general truthiness check: an explicitly passed empty list is still a list the caller supplied, and the function should use that list rather than silently replace it.

Follow-up question

Does that mean every call always gets a new list? What if I pass one in?

Geez

Only calls where items is None get a newly created list from this pattern. If a caller supplies a list, the function uses that supplied list; appending to it changes the list the caller passed. That is often useful, but it is different from the omitted-argument behavior.

For example:

shared = ["tea"]
result = add_item("bread", shared)

print(result)  # ['tea', 'bread']
print(shared)  # ['tea', 'bread']

If you want to avoid changing a caller-provided list too, make a copy inside the function before modifying it. That is a separate behavior choice: the None pattern fixes accidental sharing of the default; it does not promise that supplied arguments will remain untouched.

Follow-up question

How can I quickly review a function for this problem?

Geez

Look at each parameter default and ask whether it is a mutable object, especially a list. Then look inside the function for operations that change it, such as append, item assignment, or removal. If a mutable default is being changed, decide whether the function should maintain shared state intentionally or start fresh when the argument is omitted. For the latter, use None and initialize the object inside the function. [S1]

A compact review example:

# Risky if the function changes items
def collect(value, items=[]):
    items.append(value)
    return items

# Fresh list when omitted
def collect(value, items=None):
    if items is None:
        items = []
    items.append(value)
    return items

The Python tutorial explains the shared-default behavior and the None alternative. Choose based on whether sharing or modifying a caller’s list is part of that function’s intended behavior.

A practical checklist

  • Find list or other mutable objects used as parameter defaults.

  • Check whether the function mutates the default, for example with append or item assignment.

  • If omitted arguments should start fresh, use None and create the list inside the function.

  • Decide whether a caller-provided list is meant to be modified.

  • Compare two calls that omit the argument to check whether state carries over.

A few more useful answers

Is an empty list different from `None` when passed explicitly?

Yes. In the shown pattern, an explicitly passed empty list is used as supplied; a new list is made only when the argument is None.

Is sharing a default list always wrong?

Not necessarily. It can be intentional when shared state is desired, but it is risky when the function is expected to start with a fresh list on each omitted-argument call.

Does the `None` pattern copy a list that I pass in?

No. If you pass a list, the example uses that list and appending changes it. Make a copy first if you want to avoid modifying the caller’s list.

Sources

References used for this guide.

  1. [S1] Python Tutorial: Default argument values
    docs.python.org · Checked

YOUR QUESTION. YOUR DETAILS.

Let’s work through your situation.

Get a tailored answer or help with your next task. Starts at $1, with a quote before you pay.

Ask your own question
← Back to Geez at Work