Embercode
✕
References and MemoryLesson 15 of 15

40 min read

Aliasing in Python

Why it matters

A common bug looks like magic. You change a list in one part of a program, and a different part shows a changed list too, even though it never touched the list. You check the code and nothing seems wrong. The cause is usually aliasing: two names that refer to the same object.

Aliasing is not a mistake in Python. It is how Python works with objects, and most of the time it is what you want. The problems start when the code expects two separate copies and gets one shared object. Game save states, undo history, shared settings and cached results all have this problem. Interviewers also ask about it, because the answer shows whether you understand what a name holds.

By the end of this lesson you will be able to spot an alias in a program, predict what a change through one name does to the other, and choose the right way to make a copy. You will also see the difference between a copy and a view, which looks like a copy but still shares the data.

The earlier lesson on passing objects to functions showed that a function receives a copy of the reference, not a copy of the object. This lesson builds on that. The later lesson on shared ownership goes further, into which part of a program is responsible for an object's life.

The idea: one object, two labels

An object is a piece of data that the program creates while it runs, such as a list, a dictionary or an instance of your own class. A name is a label the program uses to reach an object. When you write b = a, you do not make a second list. You make b point at the same list that a points at. Now both names reach one list.

An alias is one of those names. The two names are aliases of each other, because they lead to the same place.

Think of a shared document. You and a friend each have a link to the same file. If your friend types a sentence, you see it on your screen. You do not have two files. You have two ways of reaching one file. Aliases in a program work the same way.

A copy is a different thing. A copy is a second object, with its own contents, that starts with the same values. Changing the copy does not change the original. Python never makes a copy by itself when you assign one name to another. You must ask for a copy with a specific method or with code you write.

Example 1: two names for one list

The simplest case uses two names for one list.

Python · editable

Type nothing into the Input box for this program.

a = [100, 2, 3] , b = [100, 2, 3]
a is b: True

The write b[0] = 100 changed the list, and a shows the change because a leads to the same list. The line a is b prints True. The is operator asks whether two names lead to the same object. The == operator would compare the contents, so two different lists with the same items would give True for == and False for is. When is returns True, the names are aliases.

The line b = a is the source of the alias. Nothing else in the program needs to be wrong. An alias forms every time two names are given the same object, whether by assignment, by passing an argument to a function, or by storing an argument in an attribute.

Example 2: a class keeps the caller's list

Aliasing often happens without an assignment that looks like one. The most common case is a class that stores a list passed in from outside.

Python · editable

Type nothing into the Input box for this program.

playlist sees: ['intro', 'outro', 'bonus']

The Playlist constructor, the __init__ method that runs when an object is made, does self.songs = songs. That line does not copy the list. It gives the attribute the same list that mine refers to. When the caller later adds "bonus" to mine, the playlist changes too. Nobody called a method on the playlist. The change arrived through the caller's name.

The class is not wrong to store the list, if the class is meant to share it. The problem appears when the class expects to own its own data. Decide which one it should be, and write the choice in the code.

Making a copy on purpose

A copy is a new object with the same contents. Python has several ways to make one. Which one you pick depends on the type of the value and on whether it contains other objects.

TypeCopy methodWhat is copied
listitems.copy(), items[:] or list(items)The list and its items, one level down
dictd.copy() or dict(d)The dictionary and its keys and values, one level down
list that must not changetuple(items)The items, into a tuple that cannot change
Any object, all levelscopy.deepcopy(obj)The object and every object inside it

Each row except the last copies one level. If the items inside are themselves lists or objects, those inner objects are not copied. The next section explains what that means.

Shallow copies and deep copies

A shallow copy makes a new outer object, but the objects inside it are the same as before. A deep copy makes new copies of the inner objects too.

Here is a list of rows, where each row is its own list. The method copy() on the outer list copies only the list of row references, not the rows.

Python · editable

Type nothing into the Input box for this program.

grid = [[9, 1], [2, 2]]
shallow = [[9, 1], [5, 5]]

Look at each line of the program.

  • shallow[0][0] = 9 changes a value inside row 0. Row 0 is shared by both lists, so grid shows the change.
  • shallow[1] = [5, 5] puts a new row into the copy. This changes only the copy's list of rows. grid[1] still holds [2, 2].

So a shallow copy protects you from changes to the outer structure, but not from changes to the rows. To copy the rows as well, you copy each row:

Python · editable

Type nothing into the Input box for this program.

grid after the copy edit: [[1, 2], [3, 4]]
copy: [[99, 2], [3, 4]]

The list comprehension creates a new row for each row of the original. The change to copy_rows[0][0] stays in the copy. The original grid is untouched.

For a structure with more levels, or with objects of your own class, use copy.deepcopy, which copies everything it finds:

Python · editable

Type nothing into the Input box for this program.

[[1, 2], [3, 4]] [[1, 2], [3, 0]]

A deep copy has to walk every object it copies, so it costs more time than a shallow copy. Deep copying is sometimes the right choice and sometimes too costly, so decide based on what the program needs.

A view looks like a copy but is an alias

Some Python methods return a view, which is another way of reaching the same data. The most common example is dict.keys(). A view is not a copy, and it shows every later change to the dictionary.

Python · editable

Type nothing into the Input box for this program.

keys view: ['a', 'b']

The name keys reaches the dictionary's keys, not a list of them. When "b" is added to the dictionary, the view shows it. To freeze the keys at one moment, make a list with list(d.keys()).

A read-only view stops changes through the view's own name. It does not stop changes through the original name. If you return a view from a method that gives out a field's value, the caller can see every later change to the original data, and the class that owns the data can still change it. For a dictionary, types.MappingProxyType gives a read-only view, and the difference from a copy is the same: the view shows later changes. If you want the caller to see a fixed snapshot, make a copy.

Python · editable

Type nothing into the Input box for this program.

view = ['x', 'y', 'z'] , snapshot = ('x', 'y')

The name view is a second name for the list, so it shows z. The tuple snapshot was made when the call ran, so it does not show z.

Defensive copies in a class

A class that stores a list or a dictionary should copy it on the way in and on the way out. The copy on the way in stops the caller from changing the class's data later. The copy on the way out stops the caller from changing the class's data through the method that returns it. These are called defensive copies.

Python · editable

Type nothing into the Input box for this program.

safe playlist: ['intro', 'outro']
AttributeError: 'tuple' object has no attribute 'append'

The constructor copies songs into a new list. The later mine.append("bonus") does not reach the playlist, which is why the output shows only two songs. The method returns a tuple, which the caller cannot change, so sp.songs().append("hack") raises an AttributeError and the playlist stays the same.

The method's copy is made each time it is called. That costs time and memory for a very long list, and it is the price of keeping the class's data safe. For a short list, the cost is usually small. For a long list that is read in a tight loop, consider storing an immutable copy once, in the constructor, and returning it without making another copy.

Harder example: snapshots in a loop

Aliasing causes its worst bugs in loops, because the same object is added to a collection many times. Here a game records the board after each move. The first version stores the board itself. The second stores a copy.

Python · editable

Type nothing into the Input box for this program.

[[1, 2, 3], [1, 2, 3], [1, 2, 3]]
[[1, 0, 0], [1, 2, 0], [1, 2, 3]]

In record_bad, the same list is added to the history three times. All three entries are aliases of one board, and the board ends in state [1, 2, 3]. So the history shows the final state three times. The player who looks back at the first move sees the last move instead.

In record_good, board.copy() makes a new list for each step. Each entry keeps the board as it was at that step. The rule to remember is simple: when a value will be stored and later changed, store a copy, not the working object.

Loop variables are aliases too

The loop variable in a for loop is another name for each item in the collection. For a list of rows, the loop variable is an alias of each row. Writing into it changes the original list.

Python · editable

Type nothing into the Input box for this program.

after loop grid = [[9, 0], [2, 0]]

The name row reaches the same list that grid holds in its first and second positions. The write row[1] = 0 changes that list. If you want the loop to work on copies, call row.copy() inside the loop, and build a new list from the copies.

Aliases through function arguments

A function receives a copy of the reference, as lesson 3 showed. That copy is an alias of the caller's object. So a function that changes the object through its parameter changes the caller's object too. A function that makes its own copy first does not.

Python · editable

Type nothing into the Input box for this program.

after add_bonus: [65, 75]
original: [60, 70] boosted: [65, 75]

The function add_bonus returns nothing. Its effect is entirely through the alias in scores, so the caller's list changed. The function with_bonus builds a new list in a list comprehension, and the caller's original list keeps its values. The caller decides what to do with boosted.

Neither style is wrong. A function that updates an object in place is clear when the caller expects that change, and the name should say so. A function that returns a new value is safer when the caller needs the original as well. Python's own functions follow the same split: list.sort() changes the list in place and returns None, while sorted() returns a new list. Pick the style that matches what the name of the function promises.

Common mistakes

Mistake 1: thinking = copies a list. It copies the reference, so both names lead to the same object. The output a is b: True in example 1 shows this. Use copy(), list(...), slicing or copy.deepcopy when you need a copy.

Mistake 2: thinking copy() copies everything inside. It copies only one level. In the grid example, shallow[0][0] = 9 changed grid. Copy each inner list as well when the rows need to be separate, or use copy.deepcopy.

Mistake 3: returning an internal list from a method. A method like return self._songs gives the caller an alias of the class's private data. Return a tuple, or a copy, if the caller must not change it. Remember that a view still shows later changes.

Mistake 4: storing a caller's list without copying. The Playlist example printed ['intro', 'outro', 'bonus'], which means the class kept the caller's list. The class did not own the data it expected to hold. Copy the list in the constructor if the class should own it.

Mistake 5: storing the same working object in a collection. The record_bad function printed three identical snapshots. The fix is to store a copy at the moment you add it.

Mistake 6: using == to check for an alias. The == operator compares contents, so two separate lists with the same items compare equal. Use is to ask whether two names lead to the same object.

Practice

Exercise 1. Predict the output of this program, then check it.

x = [1, 2]
y = x.copy()
y[0] = 5
x[1] = 7
print(x, y)

Show the answer

The output is [1, 7] [5, 2]. The copy is a separate list, so changing y[0] does not reach x, and changing x[1] does not reach y.

Exercise 2. A class stores the list of names it is given. Fix the class so that a later change to the caller's list does not change the class's data.

class Team:
    def __init__(self, members):
        self.members = members

Show the answer

Change the constructor to self.members = list(members). The class now keeps its own list. A later change to the caller's list leaves self.members as it was. A method that returns self.members should return tuple(self.members), so the caller cannot change the class's data.

Exercise 3. Predict what this prints.

g = [[1]]
h = g.copy()
h[0][0] = 3
print(g)

Show the answer

The output is [[3]]. The copy copies only the outer list. Both g and h hold the same inner list, so changing h[0][0] changes g too. To avoid this, copy each row, as the copy_rows example did, or use copy.deepcopy.

Exercise 4. A function receives a list and returns a view of it, and the caller stores the result. Later, the function's own code adds an item to the original list. Does the stored result show the new item? Explain why.

Show the answer

It depends on what the function returns. If it returns the list itself, the stored result shows the new item, because it is the same object. A tuple made with tuple(...) is a copy, so it does not. If the result must stay the same, the function should return tuple(items) or list(items), which makes a separate object at the moment of the call.

Exercise 5. Write a function copy_board that takes a list of rows and returns a deep copy, so that changes to the returned board do not reach the original.

Show the answer

def copy_board(board):
    return [row.copy() for row in board]

The list comprehension makes a new row for each row of the input. copy.deepcopy(board) gives the same result when the rows hold objects that also need copying.

Summary

  • Two names that lead to the same object are aliases, and a change through one name shows through the other.
  • Assigning a list or a dictionary to another name copies the reference, not the object. Use is to check whether two names are aliases.
  • copy(), list(...) and slicing make copies, but each copies only one level. Inner lists and objects are shared unless you copy them too, or use copy.deepcopy.
  • A view, such as dict.keys(), shows later changes to the original. A tuple or a copy does not.
  • A class that stores or returns a list should copy it on the way in and on the way out.
Check yourself: Diagrams and aliasing (8 questions) →Check yourself: Memory mixed review (6 questions) →Practice this topic: coding problems for References and Memory →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Read-only values in Python