Why do my lists get modified when passed to a function?

In the following simplistic example variables x and y are assigned and passed into a function, where, under new names, they’re modified, summed and the result returned:

x,y = 2,3
def some_function(a,b):
    z = a*2 + b*2
    return z

z = some_function(x,y)
print(x, y, z)

> 2 3 10 

Naturally, x and y have not changed, but that is what happens if they’re lists:

x = [[2,2]]
y = [[3,3]]  

def some_function(a,b):
    z = a + b                         # concatenate lists

    for i in range(len(z)):           # switch sign of 2nd elements
        z[i][1] *= -1
    return z

z = some_function(x,y)

print(f"z = {z}") 
print(f"x (now) = {x}")
print(f"y (now) = {y}")

z = [[2, -2], [3, -3]] # perfectly correct
x (now) = [[2, -2]] # original lists have been changed(!?)
y (now) = [[3, -3]]

So passing lists to a function where they’re used with different variable names causes them to be altered? Never expected that, but it certainly explains the weird results I’m getting. I thought Python functions were supposed to create new local-only variables.

Note: The original lists are not and cannot be immutable. Later I will modify them, but I will need the originals to do that.

Can someone explain what’s going on here and how I can prevent my lists from being altered when passed to a function?

Your lists get modified because lists are mutable objects that are
capable of being modified, and you modify them :slight_smile:

Objects are not copied when you pass them to a function. Any
modification you make to the object is visible outside the function.

In your example, you are using this:

x = [[2,2]]
y = [[3,3]]  

then inside a function:

z = a + b

This gives you a new list z = [[2, 2,], [3, 3]] but the inner lists
aren’t copies of the oiginal [2, 2] and [3, 3] lists, they are exactly
the same lists. Any modifications you make to the inner lists of z are
visible everywhere.

You wrote:

“So passing lists to a function where they’re used with different
variable names causes them to be altered? Never expected that, but it
certainly explains the weird results I’m getting. I thought Python
functions were supposed to create new local-only variables.”

The variables (name bindings) are local only: assigning to a local
name doesn’t change local names in other functions.

The contents of the variable are objects, not copies. If you modify
the object, you have modified the object, not a copy of the object.

You don’t need anything as complicated as your example to demonstrate
Python’s object behaviour:

L = []
def demo(a):
    a.append(1)

demo(L)
print(L)

What do you expect this to do? If you expect it to print [1], then you
expect exactly the behaviour which Python uses. If you expect it to
print the empty list [] then you will be disappointed.

You ask:

“how I can prevent my lists from being altered when passed to a
function?”

Either re-think your strategy, or make a copy of the lists (either
before passing them in, or after passing them in but before modifying
them).

For this specific example, the simplest change you could make is to
make a deep copy of z (a shallow copy isn’t sufficient):

import copy

# inside your function
z = copy.deepcopy(a + b)

but that gets very expensive for arbitrarily large and deeply nested
objects. An alternative would be:

z = [x[:] for x in a]  # Copy the sublists in a.
z.extend(x[:] for x in b)  # Copy the sublists in b.

but if you have three or more levels of nesting it won’t copy those.

2 Likes

Clear, and thank you for a detailed and thoughtful reply.

In fact, I had noted that lists are “mutable objects” but that definition did not scream out to me that using a copy in a function would mutate it. After all, x is also mutable (right? I can change it, unlike a string which I know is not mutable), but if I pass x to a function and the function alters its value under a new name the original x is still there. (My references don’t actually tell me what x is – maybe it’s not an object.)

Just trying to say I think the difference in treatment between x and L is not all that obvious and there’s some justification for confusion here, altho, of course, it’s mostly attributable to my ignorance (and isolation). Anyway, copy.deepcopy does the trick and I’m on my way. Thanks again.

1 Like

Hi Igor,

“In fact, I had noted that lists are “mutable objects” but that
definition did not scream out to me that using a copy in a function
would mutate it.”

But it’s not a copy, it’s the same object.

I suspect that you are thinking of variables using the “named bucket” or
“named memory location” model familiar from low-level languages like C.
There, if you have a variable x, and you assign it to another variable
y, the compiler makes a copy of the contents of x:

x = 2
y = x

Now y also has the value of 2, but it is a different set of bits to the
bits representing the 2 in x. So if you mutate the x bits the y bits
don’t change.

That’s not how Python works. In Python, x and y are two different names
for the same 2 object. int objects are immutable, so we can never mutate
that 2 into a 3, say, but if we used a mutable object instead of an int,
we could:

x = []
y = x

Now x and y are names for the same list object, and if you mutate x that
is implicitly mutating y.

Analogy: if Donald Trump gets a haircut, so does the President of the
United States of America, because (today) they are two names for the
same person.

“After all, x is also mutable (right? I can change it,
unlike a string which I know is not mutable), but if I pass x to a
function and the function alters its value under a new name the
original x is still there. (My references don’t actually tell me what
x is – maybe it’s not an object.)”

Names like x are not objects, they’re just names.

(To be completely pedantic, names are usually represented by string
objects in a dict somewhere, but that’s neither here nor there.)

So when you have this:

def function(y):
    pass

x = []
func(x)

the global name x and the function local name y both refer to the
same list object. Just like Donald Trump’s hair, if you modify y you
also modify x because they both refer to the same object.

1 Like

OK, so int objects are immutable, which explains this:

>>> x=2
>>> y=x
>>> x=x**2
>>> x
4
>>> y
2

Given your explanation and examples above re how two names refer to the same thing, should I not expect y to change if x and y are linked mutable objects:

>>> x=[1]
>>> y=x
>>> x=x+x
>>> x
[1, 1]
>>> y
[1]

Why does y not get mutated in this case?

Because the + operator in x+x creates a new list. You then assign the new list to x (in x = ...), but y still refers to the old one.
(Continuing with the analogy, that’s like: “Jane Doe got a haircut, and got elected as the President of the USA. Does that make Donald Trump’s hair shorter?”)

Try the same with a mutating method like append:

>>> x = [1]
>>> y = x
>>> x.append(2)
>>> x
[1, 2]
>>> y
[1, 2]
1 Like

To follow on from Petr’s comment, assignment is not mutation. If two
names refer to the same object:

x = y = [1, 2]

but you assign a new value to one of the names, that doesn’t mutate the
object, it changes what the name refers to without affecting the other
name:

x = x + [3, 4]  # assignment to a new object
assert y == [1, 2]  # the original object hasn't changed

The only gotcha to watch out for is that augmented assignment can look
like assignment, and we often call it assignment, but it is actually a
hidden method call which might be mutating.

> x = y = [1, 2]  # mutable
> x += [3, 4]
> y
[1, 2, 3, 4]

But:

> a = b = (1, 2)  # immutable
> a += (3, 4)
>>> b
(1, 2)

There is a long story for why it has to be that way. If you really care
you can read the PEP:

but my recommendation is that you avoid augmented assignment until you
are more comfortable with the basics.

1 Like

The statement in the PEP, “The precise action performed on the two arguments depends on the type of x, and possibly of y” basically tells me that I’ll just have to watch out for method calls that “might” be mutating without fully understanding why.

FYI, in my case I have a master list of large numbers expressed in prime-factor/exponent format. I use slices of this list in various functions and I can’t have them corrupted, so I’m passing each one as a deepcopy and all is well.

Thanks for the attempts to enlighten me.

1 Like

This has always been the case with Python, though. If you want to be 100% assured that your arguments aren’t mutable, use unmutable types such as tuple.

1 Like

Explaining your use-case makes a big difference.

So you have a numbers written in this form:

# 150 = 2*3*(5**2)
[[2, 1], [3, 1], [5, 2]]

and then you make a list of those numbers, correct?

Easy fix: write your numbers as tuples, not lists:

((2, 1), (3, 1), (5, 1))  # 150

and each number is now immutable and cannot be modified. Now the only
data structure you have that is mutable is the master list itself:

numbers = [((2, 1), (3, 1), (5, 1)),
           ((2, 3), (7, 2), (13, 1)),
           ]

which may not matter to your code. But if it does matter, you only need
a shallow copy of that, which is easy enough to get with a slice:

numbers[:]

I did think of tuples. My prime-factor/exponent pairs are generated by a function that fills a list. I don’t know if you can put them directly into a tuple instead, or if you have to convert the list somehow.

I found there’s a tuple function, tuple([iterable]), but when I put my list in there I got error messages so I gave that up. That was the last thing I tried before appealing to you guys.

You can construct tuples by separating items with a comma:

t = (prime, exponent)

If you construct a list of these, you can convert to a tuple when you
are done:

items = []
items.append((2, 1))
items.append((3, 1))
items.append((5, 2))
print(items)
# => [(2, 1), (3, 1), (5, 2)]
items = tuple(items)
print(items)
# => Now an immutable tuple ((2, 1), (3, 1), (5, 2))
1 Like

I converted to tuples and still had a problem. I then realized that if a tuple couldn’t be changed it must be getting replaced. On that line I found where it was happening and corrected the problem.

Thanks for all the good input.