The four lines#
Read this and predict the output before you scroll:
x = [0, 1, 2]
y = x
x[0] = 99
print(y)
[99, 1, 2]
Nothing was ever done to y. The third line only mentions x. And yet y changed.
Almost everyone reads line 2 as “make a copy of x and call it y.” That is not what it does. It makes a second name for the one list that already exists. After line 2 there is still only one list in memory; it just answers to two names. Line 3 reaches that list through the name x, and line 4 looks at the same list through the name y.
The word for this is aliasing: two or more names bound to one object.
Why numbers and strings seem immune#
Here is the same shape, with an integer instead of a list:
a = 5
b = a
a = 99
print(b)
5
This is the comparison exams love, because the two results look contradictory and are not. The difference is not that numbers get copied and lists do not. The difference is what line 3 does.
x[0] = 99mutates — it reaches inside the existing list and changes part of it. Everyone looking at that list sees the change.a = 99rebinds — it points the nameaat a different value entirely. The old value is untouched, andbis still pointing at it.
Integers, floats, booleans, strings and tuples are immutable: there is no operation that changes one in place, so rebinding is the only thing that can ever happen to them. Lists and dictionaries are mutable, so both operations are available — and only one of them is visible to the other name.
Strings make the point cleanly, because they behave like the number even though they look like a list of characters:
word = "hello"
word2 = word
word = word + "!"
print(word2)
hello
word + "!" cannot modify "hello" — it builds a brand-new string and rebinds word to it. Try to mutate a string directly and Python stops you outright:
greeting = "Hello, world!"
greeting[0] = "J"
Traceback (most recent call last):
File "greet.py", line 2, in <module>
greeting[0] = "J"
~~~~~~~~^^^
TypeError: 'str' object does not support item assignment
Assignment never copies. It only ever binds a name. Whether that matters depends entirely on whether the thing bound can be mutated.
Proving it with is#
== asks “do these hold the same values?” is asks the sharper question: “are these the same object?”
x = [1, 2]
y = x
z = x[:]
print(x is y)
print(x is z)
print(x == z)
True
False
True
y is x — one list, two names. z is a different list that happens to contain equal values. That is exactly the distinction the first example turned on, and is lets you see it directly.
How to actually copy a list#
x = [0, 1, 2]
y = x[:]
x[0] = 99
print(x)
print(y)
[99, 1, 2]
[0, 1, 2]
Three ways to say the same thing, all fine:
| Written as | Reads as |
|---|---|
y = x[:] | a slice of everything — the one your course is most likely to use |
y = list(x) | build a new list out of the items of x |
y = x.copy() | the explicit one, and the easiest to read aloud |
The same rule inside functions#
Passing a list to a function does not copy it either. The parameter is one more name for the caller’s list, so a function that mutates it changes the original. A function like this is called a modifier:
def add_one(alist):
for i in range(len(alist)):
alist[i] += 1
nums = [1, 2, 3]
add_one(nums)
print(nums)
[2, 3, 4]
Note that add_one returns nothing and the caller never assigns anything — and nums changed anyway. Compare a pure function, which builds a new list and hands it back:
def add_one(alist):
new_list = []
for item in alist:
new_list.append(item + 1)
return new_list
nums = [1, 2, 3]
result = add_one(nums)
print(nums)
print(result)
[1, 2, 3]
[2, 3, 4]
Same name, same call, opposite effect on nums. Neither style is wrong — pure functions are easier to reason about because they have no hidden side effects; modifiers avoid building a second list. What matters is knowing which one you wrote.
And the number case behaves exactly as it did before:
def bump(n):
n += 1
x = 5
bump(x)
print(x)
5
n += 1 on an integer rebinds the local name n. The caller’s x never moves.
On a list, += is not shorthand for x = x + [...]. It extends the list in place, so the alias sees it:
x = [1, 2]
y = x
x += [3]
print(y)
[1, 2, 3]
Whereas the spelled-out version builds a new list and rebinds x, leaving y where it was:
x = [1, 2]
y = x
x = x + [3]
print(y)
[1, 2]Methods that return None#
The mutable/immutable split shows up one more way, and it produces a bug that looks nothing like its cause:
mylist = [3, 1, 2]
x = mylist.sort()
print(x)
print(mylist)
None
[1, 2, 3]
.sort() sorted the list — it just did not hand it back. Methods that mutate a list conventionally return None, so assigning their result throws away the list and keeps the nothing. The symptom usually arrives a few lines later, pointing at a line that is not the mistake:
mylist = [3, 1, 2]
x = mylist.sort()
print(x[0])
Traceback (most recent call last):
File "sortdemo.py", line 3, in <module>
print(x[0])
~^^^
TypeError: 'NoneType' object is not subscriptable
Two correct forms, and they are not interchangeable. Sort in place and keep the original name:
mylist = [3, 1, 2]
mylist.sort() # sorts in place, changes the original
print(mylist)
[1, 2, 3]
Or build a new sorted list and leave the original alone:
mylist = [3, 1, 2]
new = sorted(mylist) # builds a new list, original untouched
print(mylist)
print(new)
[3, 1, 2]
[1, 2, 3]
The same “returns None” rule covers .reverse(), .append(), .insert(), .extend(), .remove() and .clear(). The exceptions worth knowing are .pop(), which removes an item and returns it, and .index() and .count(), which do not mutate at all.
Tuples, the immutable one#
A tuple is an ordered sequence like a list, with one difference that explains everything else about it — you cannot change it after it is built:
point = (3, 4)
point[0] = 5
Traceback (most recent call last):
File "pt.py", line 2, in <module>
point[0] = 5
~~~~~^^^
TypeError: 'tuple' object does not support item assignment
Because a tuple cannot be mutated, aliasing a tuple is as harmless as aliasing a number. That is the whole reason to reach for one: it is a record you can hand around without worrying who might change it behind your back.
Tuples are also why a function can appear to return more than one value:
def quotient_and_remainder(a, b):
return a // b, a % b
q, r = quotient_and_remainder(17, 5)
print(q, r)
3 2
There is no special “multiple return” feature here. return a // b, a % b builds one tuple, and q, r = unpacks it into two names. Catch it in a single variable and you can see the tuple plainly:
def quotient_and_remainder(a, b):
return a // b, a % b
both = quotient_and_remainder(17, 5)
print(both)
print(type(both))
(3, 2)
<class 'tuple'>
Unpacking works on both sides of an assignment at once, which is why swapping needs no temporary variable:
a = [1, 2, 3]
a[0], a[-1] = a[-1], a[0]
print(a)
[3, 2, 1]
Copies are only one level deep#
This is past what most first-semester exams ask, and it is worth two minutes because it is the same idea one layer down. x[:] builds a new outer list — but it fills that list with the same inner objects:
board = [[0, 0], [0, 0]]
copy = board[:]
copy[0][0] = 9
print(board)
[[9, 0], [0, 0]]
The outer lists are genuinely separate; the two inner lists are shared. This is called a shallow copy, and it is what all three copying techniques above give you.
The same mechanism produces a classic grid bug:
row = [0] * 3
grid = [row] * 2
grid[0][0] = 9
print(grid)
[[9, 0, 0], [9, 0, 0]]
[row] * 2 did not make two rows. It made one row, listed twice. Build grids with a loop or a comprehension — [[0] * 3 for _ in range(2)] — so each row is a separate list.
Where these go wrong#
- “Backing up” a list before changing it.
original = datathen editingdataleaves you with two names for the edited list and no backup at all. Useoriginal = data[:]. - Assigning the result of
.sort().x = mylist.sort()silently putsNoneinx. Nothing complains until you usex. - Expecting a function not to touch your list. If it calls
.append()or assigns toalist[i], your list changed — even if the function returns nothing. - Expecting a function to change your number. The mirror image, and just as common. Return the new value and assign it.
- Mixing up
==andis. Two separate lists with the same contents are==but notis. - Building a grid with
*.[[0] * 3] * 2gives you one row referenced twice, and every “row” changes together. - Trying to mutate a tuple or a string. Both raise
TypeError: ... does not support item assignment. Build a new one instead.
Test yourself in the free Kestrel Exams app
Topic-selectable practice — offline, no ads, no account.
See the Python course →Frequently asked questions#
Why did changing x also change y in Python?
Because y = x did not copy the list. It gave the one existing list a second name. Both names refer to the same object, so a change made through either name is visible through the other. To get an independent list, use y = x[:], y = list(x) or y = x.copy().
Why does the same line not cause problems with numbers or strings?
Numbers and strings are immutable, so there is no way to change one in place. a = 99 does not modify the old value, it makes a refer to a different value, and b still refers to the original. The aliasing is still there; it is simply harmless because nothing can be mutated.
How do I copy a list in Python?
Use a full slice y = x[:], the list constructor y = list(x), or y = x.copy(). All three build a new list. Note that all three are shallow: if the list contains other lists, the inner lists are still shared.
Why does my list become None after I sort it?
Because .sort() sorts the list in place and returns None, so x = mylist.sort() stores None in x. Call mylist.sort() on its own line, or use new = sorted(mylist), which returns a new sorted list and leaves the original alone. The same applies to .reverse(), .append() and .remove().
Does changing a list inside a function change the caller's list?
Yes, if you mutate it. A parameter is another name for the same list, so alist[i] = 0 or alist.append(x) is visible to the caller. Rebinding the parameter with alist = [] is not, and neither is n = n + 1 on a number parameter.
What is the difference between a list and a tuple?
A tuple is immutable: once built, you cannot change an item in it. point[0] = 5 raises TypeError: 'tuple' object does not support item assignment. That makes tuples safe for fixed records and is why returning several values from a function returns a tuple.
