Skip to content
← Back to explainers

Python explainer

When Two Names Share One Object

About 3 min to read. Allow extra time to work through examples.

See why assignment shares an object instead of copying it, how a function can change its caller's list, and what a shallow copy copies.

This article uses Python. You can make it your practice mode to browse related content across the site.

You pass a list to a helper function, and when it returns, your list has changed. Nothing in your code assigned to it. Here is the smallest version:

original = [1, 2, 3]
alias = original
alias.append(4)
print(original)  # [1, 2, 3, 4]
print(alias is original)  # True

alias = original does not copy the list. It gives the same list a second name. A change made through either name shows through the other, because there is only one list.

Assigning a new object is different. alias = [9] points alias at a new list and leaves original as it was.

A function shares its arguments with the caller

A parameter is one more name for the object the caller passed in. If the function changes that object in place, the caller's object changes. If it assigns a new object to the parameter, only the local name moves:

def change(items: list[int]) -> list[int]:
    items.append(2)  # changes the caller's list
    items = [9]  # points the local name at a new list
    return items


values = [1]
returned = change(values)
print(values)  # [1, 2]
print(returned)  # [9]

append, extend, sort, remove and clear change a list in place. +, sorted() and slicing build a new list. When a function should leave its argument alone, use the second group, or copy the argument first.

+= changes a list in place too. On a tuple or a string it builds a new object, because those types cannot change.

A shallow copy shares what is inside

list(items), dict(d), items.copy() and copy.copy(x) all make a shallow copy: a new outer container that holds the same inner objects.

defaults = {"retries": {"max": 3}}
first = dict(defaults)
first["retries"]["max"] = 5
print(defaults["retries"]["max"])  # 5

first is a new dictionary, but first["retries"] and defaults["retries"] are the same inner dictionary. Copy the inner level you plan to change as well:

defaults = {"retries": {"max": 3}}
independent = {"retries": dict(defaults["retries"])}
independent["retries"]["max"] = 7
print(defaults["retries"]["max"])  # 3

For each function, decide which objects it owns and which it shares with its caller. A type hint such as Sequence[str] copies nothing. Copying every level at once, with copy.deepcopy, comes later in this track.

Check your understanding Would a tuple prevent every bug where shared data changes? Show answer

No. A tuple cannot change which objects it holds, but one of those objects can still be a list or a dictionary. A change to that list shows through every name that shares it. Use unchangeable items too, such as strings, numbers and other tuples, when the whole value must stay fixed.

Create a default list inside the function

Python evaluates a default value once, when it runs the def statement. A default list is therefore one list, shared by every call that leaves the argument out. Use None as the default, and create the list in the body:

def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:
    if tags is None:
        tags = []
    tags.append(tag)
    return tags


print(add_tag("python"))  # ['python']
print(add_tag("typing"))  # ['typing'], not ['python', 'typing']
Check your understanding Does the None default stop this function changing its argument? Show answer

No. A call that leaves tags out gets a fresh list. A call that passes its own list still has that list changed. If the function should never change its argument, copy the list the caller passes before you append to it.

is asks about the object, == about the value

left = [1, 2]
right = [1, 2]
print(left == right)  # True: equal values
print(left is right)  # False: two different lists

Use is to check for None. Use == to compare values. A later article, "When to Use is and When to Use ==", covers the cases in between.

id(obj) returns a number that is unique among the objects that exist at that moment. CPython can reuse the number once it frees the object, so never store it as a record ID. "What Keeps an Object Alive", later in this track, explains when CPython frees an object.

step 1 of 38

Memory, Copies and Slow Code

Up next Fix a Function That Changes Its Caller's List