5 Common Mistakes That Every Python Programmer Should Avoid
Python is one of the languages that has been witnessing incredible popularity in recent years. It is widely used because of its versatility, readability, efficiency, a huge mature and supportive Python community. Python’s simplicity and readability may often mislead many programmers (especially the new ones). This article covers the top 5 most common mistakes that every Python programmer should avoid.
Photo by chuttersnap on Unsplash
1. Use of a Bare Except Clause to Catch Exceptions
We should avoid using bare except clauses to catch exceptions as they catch every system exception. In the below example - the program supposed to continue to run even if it catches any exception.
while True: try: print("I'm in try block") sleep(1) except: print("exception caught here")
The problem with this program is that it will catch many exceptions, even those which are supposed to be skipped. For example - system exits and keyboard interruptions. This means if you try to stop this program via a keyboard interruption (CTL+ C) or call a sys.exit() method within the scope of try block then the program won’t stop. Such issues can be avoided by mentioning an explicit exception class name in the except clause.
We should at least go with a bare minimal approach by mentioning a particular class name (Exception class mostly) in the except clause.
Here is equivalent code (with an Exception class included) :
while True: try: print("I'm in try block") sleep(1) except Exception: print("exception caught here")
Mentioning the Exception class in the except clause will ensure that it catches the exceptions which are handled by either Exception class or other classes driven from this. You can find Python’s built-in class hierarchy for exceptions here - https://docs.python.org/3/library/exceptions.html#exception-hierarchy.
2. Committing Bytecode (.pyc) Files into the Version Control System
Whenever Python executes a script/module, then the interpreter automatically writes the byte-code version of the same file and store it on the disk, mostly with a .pyc extension (i.e. my_module.pyc). The interpreter does this for performance reasons. With no byte-code files, the interpreter would re-generate the same byte-code files every time it executes the scripts.
Having the byte-code files sometimes leads to strange issues like if you delete the actual source script/module then the interpreter can still load that module (from bytecode file). If you work in a team then this can be a big problem. As the interpreter auto-generates, these files on each machine so adding them into your version control repositories has no major benefits.
Popular version control systems like git have the ability to ignore files/directories by defining rules. Git uses .gitignore file to define such rules and the same can be used to ignore byte-code files.
Example of .gitignore file:
*.pyc # Will ignore .pyc files. __pycache__/ # will ignore the whole __pycache__ directory
You can also disable auto-generation of these byte-code files on a machine by setting environment variable PYTHONDONTWRITEBYTECODE.
$ export PYTHONDONTWRITEBYTECODE=1
3. Use of Wildcard (*) Import
from my_module import *
In Python wildcard (*) is used to import everything from a module, and many programmers (especially recent ones) use it intensively without knowing its caveats. Programmers follow this practice to save themselves from changing import statements every time the program requires an additional method or attribute from an imported module. We also call this as an anti-pattern.
Suppose your program requires scandir method from os module, then import statement for that would be (without *):
from os import scandir
Now if you also require the rmdir method from the same os module, then you will have to add this in the import statement.
from os import scandir, rmdir
This makes new programmers use * over explicit names of methods/attributes in the import statements. Though the use of simple import os would have also helped them, maybe they don’t enjoy using those module names - i.e. os.scandir(). Wildcard imports are fine as long as you are working on small one-time scripts.
Let us consider the same above example and use wildcard import this time instead of using specific names.
from os import *
Assume we have a large program where scandir and rmdir methods are invoked in the middle or at the end of the program. If things go wrong in the program then anyone will get confused to figure out where these methods are defined ?. Wildcard import makes things very implicit and hard to trace.
Wildcard import can also lead to many problems when you import two or more modules which also have methods/attributes with the same name. In such cases, the last import statement unknowingly overrides the methods/attributes of the previous import statement. For example, Python modules - glob and html both have escape() method, and both serve different purposes. If we import both modules with wildcard import:
from html import * from glob import *
If we invoke the escape() method anywhere in our program then it will always call the escape() method defined in the glob module.
4. Use of Mutable as Default Value in Function Arguments
This one is also a very common mistake that every new python programmer tends to make. The use of mutable default arguments in Python function should be avoided unless you have a solid reason to do that. This leads to a lot of confusion, unwanted results, and a waste of time to debug them.
The mutable referred here can be anything such as a list, a dictionary, or even a class instance.
Example: Function add_me() takes 2 arguments and the second argument is set to use a mutable value (a list) as default value.
def add_me(element, my_list=): my_list.append(element) return my_list print(add_me(1)) print(add_me(2)) print(add_me(3))
 [1, 2] [1, 3]
Wondering? What went wrong here? We expected to get a new result every time function add_me() gets invoked but it is not happening. Did you notice how Python retains the default value (of my_list) and ties it to the function in some way? In Python, using a mutable as default value in function arguments retains its value even after the function call completes.
The best solution to defeat is this problem is to set the default value to None and assign the mutable value inside the function.
def add_me(element, my_list=None): if my_list is None: my_list =  my_list.append(element) return my_list print(add_me(1)) print(add_me(2)) print(add_me(3))
  
5. Misunderstanding LEGB Rule
In Python, LEGB is a concept that defines rules for scope resolution and it stands for Local, Enclosing, Global, Built-in scopes. The scope rules determine how names and variables are looked up in your code.
Ordering of the scope is straightforward, and having a good understanding of scope rules can help programmers to avoid or minimize bugs related to name clash and inappropriate use of global names across your programs. Sometimes inappropriate use of this concept also leads to more common programming problems.
counter = 10 def increment(): counter += 1 return counter print(increment()) Traceback (most recent call last): File "legb.py", line 6, in <module> print(increment()) File "legb.py", line 3, in increment counter += 1 UnboundLocalError: local variable 'counter' referenced before assignment
The above error is thrown because the variable counter is declared in global scope/namespace but is being computed inside the increment() function (a local scope for this function). In Python, when you make an assignment to a variable in scope then that variable automatically is considered local to that scope.