This article discusses about common gotchas in python with code snippets describing what someone might think it would work like but how it actually works behind the scenes.
Table of Contents
- Mutable Default Arguments
 
- Late Binding Closures
 
- List Copy
 
- Local Variable
 
Mutable Default Arguments
def append_to(element, to=[]):
    to.append(element)
    return to
my_list = append_to(2)
print(my_list)
my_other_list = append_to(4)
print(my_other_list)
 
What You Might Have Expected to Happen
A new list is created each time the function is called if a second argument isn’t provided, so that the output is:
What actually happens
A new list is created once when the function is defined, and the same list is used in each successive call.
Python’s default arguments are evaluated once when the function is defined, not each time the function is called. This means that if you use a mutable default argument and mutate it, you will and have mutated that object for all future calls to the function as well.
What you should do instead
Create a new object each time the function is called, by using a default argument to signal that no argument was provided (None is often a good choice).
def append_to(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to
 
Late Binding Closures
def create_multipliers():
    return [lambda x: i * x for i in range(5)]
for multiplier in create_multipliers():
    print(multiplier(2))
 
What You Might Have Expected to Happen
A list containing five functions, That each have their own closed-over i variable that multiplies their argument, producing:
What actually happens
Five functions are created but all of them just multiply x by 4.
Python’s closures are late binding. This means that the values of variables used in closures are looked up at the time the inner function is called.
Here, whenever any of the returned functions are called, the value of i is looked up in the surrounding scope at call time. By then, the loop has completed and i is left with its final value of 4.
What you should do instead
You can create a closure that binds immediately to its arguments by using a default argument.
def create_multipliers():
    return [lambda x, i=i : i * x for i in range(5)]
 
List Copy
array1 = [1, 2, 3, 4, 5]
array2 = array1
array2[0] = 10
print(array1)
print(array2)
 
What You Might Have Expected to Happen
- array1 has values 1, 2, 3, 4, 5
 
- array2 is created with same values as like array1
 
- array2 first element is modified to 10
 
- array1 values are not changed
 
[1, 2, 3, 4, 5]
[10, 2, 3, 4, 5]
 
What actually happens
- Variables are simply names that refer to objects.
 
- Doing array2=array1 doesn’t create a copy of the list
 
- It creates a new variable array2 that refers to the same object array1 refers to.
 
- This means that there is only one object (the list), and both array1 and array2 refer to it.
 
- Lists are mutable, which means that you can change their content.
 
[10, 2, 3, 4, 5]
[10, 2, 3, 4, 5]
 
What you should do instead
Create a another object with same values of array1 using list copy method or slicing or list method.
array2 = array1.copy()
# or
array2 = array1[:]
# or
array2 = list(array1)
 
Local Variable
Consider the below two code snippets
x = 1
def foo():
    print(x)
foo()
 
x = 1
def foo():
    x += 1
    print(x)
foo()
 
What You Might Have Expected to Happen
- First code snippet prints x value 10
 
- Second code snippet should be x = x+1 and print 11
 
snippet1: 10
snippet2: 11
 
What actually happens
- When you make an assignment to a variable in a scope, that variable becomes local to that scope and shadows any similarly named variable in the outer scope.
 
- Since the first statement in foo assigns a new value to x, the compiler recognizes it as a local variable.
 
- Consequently when the next statement print(x) attempts to print the uninitialized local variable and an error results.
 
snippet1: 10
snippet2: UnboundLocalError: local variable 'x' referenced before assignment
 
What you should do instead
Use the global keyword to access the outer scope variable.
x = 10
def foo():
    global x
    x += 1
    print(x)
foo()
 
References