Pass by assignment
About this video
Python passes arguments to functions by assignment: the parameter receives a reference to the original value, not a copy. Reassigning the parameter only moves the local variable, but mutating a mutable argument like a list inside the function also changes the caller's list. Because these side effects cause bugs, it's best to build and return a new list, and to clearly document and name any function that must mutate its argument.
You'll be able to
- explain what pass by assignment means in Python.
- predict whether a change inside a function affects the original variable.
- explain why mutating list arguments can cause unexpected side effects.
- avoid side effects by accumulating and returning a new list.
Key words
- pass by assignment
- Python's way of passing arguments: the parameter gets a reference to the original value, not a copy.
- reference
- A pointer to where a value is stored in memory.
- reassign
- Point a variable at a completely new value using =
- mutate
- Change a value in place, like updating one element of a list.
- side effect
- A change a function makes outside itself, like modifying a list that was passed in.
Transcript
- [Instructor] If lists are mutable, what happens when we pass a list argument to a function? To answer this, it's time to get precise about how argument passing works in Python. For the most part, we've been passing primitive data types as arguments that is strings, integers, floats, and booleans, and we've seen that if we reassign the parameter inside the function body that only modifies the variable in local scope, it does not affect the value of the variable that we passed in.
In Python, however, it's not quite true that the computer makes a copy of the value when it passes it into the function. Instead, Python passes all arguments into functions by assignment. Pass by assignment means that the computer passes a reference to the original value, not a copy of it.
So our local parameter variable initially points that same location in memory. However, this doesn't form a permanent two-way link. If we later reassign that parameter inside the function body, it doesn't affect that original variable. The computer allocates a new chunk of memory in local scope, stores the new value there, and then moves the parameter variable to point to that new location.
Now, if we pass a list as an argument, instead the same thing happens. Instead of copying the entire list, which could be very expensive if the list has thousands of elements, the computer passes a reference to where that list is stored. If we fully reassign the parameter variable, just like before the computer allocates a new chunk of memory in local scope, stores the new list there and then moves the parameter variable to point to that new location.
But what if instead of reassigning the parameter variable, we just mutated it? Since lists are a mutable data type, we can update them in place. So if I were to update the element at index zero, this mutates the existing list. It doesn't create a new one. If I put that mutation instead inside the function body, the same thing happens.
Notice however that with pass by assignment, my parameter variable and that original variable are both pointing to the same location in memory. So any mutations I make inside the function body also affect the value of the original variable. This is a key property of pass by assignment.
With immutable types like floats, we can never mutate the original value. All operations always create a new float. So when we pass them as arguments to a function, our function doesn't affect the original variable. With mutable types, however, like lists, we do have operations that can modify the original value.
So when we pass them as arguments to a function, any mutations we make inside the function body also affect the original variable. Cool. So what do I do with this information? Well, this property puts our programs at risk of bugs because our functions can now have unexpected side effects.
I may pass my list into a function, but still wanna use it later, and you never know if that function passes the list into another function, to another function into another function. If somewhere along the line one of those functions secretly mutates my list, the list that I'm operating on over here may no longer be the version of the list that I wanted, and now my calculations are off.
Because function side effects make it more unpredictable for the programmer, it's a best practice to avoid mutating list arguments inside of functions or possible we should accumulate a new list and return it leaving the original list parameter unchanged. For example, if we wanna modify the elements of a list, we might instead create a new empty list, iterate over the original list parameter, and then append a modified version of each element to our new list.
Now, in some cases, it's true, we may need our function to mutate the list parameter, either because the purpose of the function is to have that side effect, or maybe we're operating on a list with millions of values and copying or iterating over that list would be prohibitively expensive.
In these cases, we need to make sure to clearly document the side effect and to make the side effect apparent in the function name. In summary, Python passes arguments to functions by assignment. If your argument is an immutable type, there's nothing to worry about.
But if your argument is a mutable type, any updates you make to the list parameter inside the function body carry over to the original variable you passed in.
Video © Khan Academy, used under CC BY-NC-SA. Watch on Khan Academy. Puzzicle is not affiliated with Khan Academy.