Some years ago I embarked into the adventure of learning Python, I already knew some other programming languages like PHP (the first language which introduced me to web development), JavaScript (to which I was already pretty good at it, and was writing a UI library) and C# which was the responsible for my income at the time.
I learned Python by working on an app on my own, and thus I incorporated many JavaScript and C# way of doing things into my code, which was terrible, though sometimes it worked. It took me some time, reading other's people code and working with others to actually become better at the language. Today I'd like to go through with you on some of the mistakes I did (code-wise) when learning Python.
#1 Misunderstanding Python scopes
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
nonlocal
as 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.
#2 Modifying a list while iterating over it
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:
odd = lambda x : 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:
odd = lambda x : 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:
even = lambda x : not bool(x % 2)
numbers = [n for n in range(10)]
numbers = list(filter(even, numbers))
numbers
-----------
Output
-----------
[0, 2, 4, 6, 8]
#3 Variable binding in closures
I'd like to start with a quiz I posted on twitter (@livecodestream) 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.
#4 Name clashing with Python Standard Library modules
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.
Conclusion
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:
Make Your Code Great, Python Style
Thanks for reading!
If you like the story, please don't forget to subscribe to our free newsletter so we can stay connected: https://livecodestream.dev/subscribe
Top comments (0)