Python scope resolution is based on what is known as the LEGB rule, which is shorthand for Local, Enclosing, Global, Built-in. Even though looks very simple, it was a bit confusing for me at the time, for example, consider the following example:
x = 5 def foo(): x += 1 print(x) foo() ----------- Output ----------- Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment
For the code above I’d have expected it to work, and altering the global variable
x to finally printing
6. But, it can get weirder, let’s look at the following altered code:
y = 5 def foo_y(): print(y) foo_y() ----------- Output ----------- 5
What in the world is going on? In one code snippet, the global variable
X gives an
UnboundLocalError however, when we just try to print the variable it works. The reason has to do with scoping. When you make an assignment to a variable in a scope (e.g. the function scope), that variable becomes local to that scope and shadows any similarly named variable in the outer scope. This is what happened in the first scenario when we did
x += 1.
If what we are intending is to access the global variable
x as in the case of our function
foo() we could do something like:
x = 5 def foo(): global x x += 1 print(x) foo() ----------- Output ----------- 6
By using the keyword
global allows the inner scope to access the variable declared in the global scope, meaning variables that are not defined in any function. Similarly, we could use
nonlocal to produce a similar effect:
def foo(): x = 5 def bar(): nonlocal x x += 1 print(x) bar() foo() ----------- Output ----------- 6
global allows you to access variables from an outer scope, however, in the case of
nonlocal, you can bound to an object on a parent scope or the global scope.
Though this error is not only common to Python, it’s an error that I’d find out it’s pretty common among new Python developers, and even to some experienced developers as well. Though sometimes may not seem so obvious, under certain occasions we end up modifying the array we are currently iterating resulting in unappropriated behavior, or if we are lucky we get an error and easily notice it.
But let me give you an example of what I mean, let’ say that given an array you need to reduce that array to contain only the even elements, you may attempt to do something like:
def odd(x): return bool(x % 2) numbers = [n for n in range(10)] for i in range(len(numbers)): if odd(numbers[i]): del numbers[i] ----------- Output ----------- Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range
In the scenario described, when deleting an element for a list or array while iterating, we get an error as we try to access an item that is not there anymore. This is a bad practice and should be avoided, there are better ways to achieve similar things in python, among them list comprehensions:
def odd(x): return bool(x % 2) numbers = [n for n in range(10)] numbers[:] = [n for n in numbers if not odd(n)] print(numbers) ----------- Output ----------- [0, 2, 4, 6, 8]
You could also use the
filter function to achieve the same, and though it works some argue is not the Pythonic way of doing it, and I kind of agree, but I don’t want to get into the middle of that discussion. I’d rather give you the options, and you can research and decide:
def even(x): return not bool(x % 2) numbers = [n for n in range(10)] numbers = list(filter(even, numbers)) numbers ----------- Output ----------- [0, 2, 4, 6, 8]
I’d like to start with a quiz I posted on twitter (@bajcmartinez) where I asked people about what they think the result of the following snippet would be:
def create_multipliers(): return [lambda x : i * x for i in range(5)] for multiplier in create_multipliers(): print(multiplier(2)) ----------- Output ----------- 8 8 8 8 8
For many people, myself included, the first time we encounter this problem we think that the result will be:
0 2 4 6 8
However, the code actually resulted in something totally different and we are very puzzled as to why. What is actually happening is that Python would do a late-binding behavior, according to which the values of variables used in closures are looked up at the time the inner function is called.
So in our example, whenever any of the returned functions are called, the value of
i is looked up in the surrounding scope at the time it is called.
A solution to this problem may seem a bit hacky, but it actually works
def create_multipliers(): return [lambda x, i=i : i * x for i in range(5)] for multiplier in create_multipliers(): print(multiplier(2)) ----------- Output ----------- 0 2 4 6 8
By using the default argument of the lambda function to pass the value of
i we can generate the functions to do the desired behavior. I was very puzzled by this solution, and I still consider it to be not very elegant, however, some people love it. If you know another possible solution to this problem, please let me know in the comments, I’d love to read about it.
This issue was actually pretty common when I was starting, and even now, sometimes I make this mistakes. The issue comes as a result of naming one of your modules with the same name as a module in the standard library that ships with Python. (for example, you might have a module named email.py in your code, which would be in conflict with the standard library module of the same name).
Perhaps the name clashing by itself won’t generate any issues with your code, but sometimes we override a function or module of the Python standard library, which is later used in an installed library, and it conflicts either by throwing errors or misbehaving. In any case, it’s a bad situation to have.
A classic mistake is the following:
a = list() print(a) list = [1, 2, 3] # This is where we break it a = list() ----------- Output -----------  Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'list' object is not callable
By simply creating a variable named
list we broke the access to the
list function. And, even though there are other ways of accessing it (e.g.
__builtins__.list()), we should avoid this kind of name.
This article does not cover all the common mistakes developers do when coding in Python, but rather those things I struggled the most. If you want to know more about how to write great Python code and avoiding some other mistakes I recommend you to read:
Thanks for reading!