10 common mistakes Python programmers make in development

  • 2020-04-02 13:48:02
  • OfStack

Python is an easy to learn programming language, with concise and clear syntax, and a rich and powerful library of classes. Unlike most other programming languages, which use braces, it USES indentation to define blocks of statements.

Python developers are prone to making mistakes that are easy to avoid at work. Here are 10 of the most common mistakes Python developers make.

1. Misuse of expressions as default values for function arguments

Python allows developers to specify a default value for function arguments, which is a feature of the language, but can easily lead to confusion when arguments are variable. For example, the following function definition:


>>> def foo(bar=[]):        # bar is optional and defaults to [] if not specified
...    bar.append("baz")    # but this line could be problematic, as we'll see...
...    return bar

In the above code, once the foo() function is called repeatedly (without specifying a bar parameter), it will always return 'bar', because no parameters are specified, and each time foo() is called, it will assign []. Here's what happens:

>>> foo()
["baz"]
>>> foo()
["baz", "baz"]
>>> foo()
["baz", "baz", "baz"]

Solutions:


>>> def foo(bar=None):
...    if bar is None:  # or if not bar:
...        bar = []
...    bar.append("baz")
...    return bar
...
>>> foo()
["baz"]
>>> foo()
["baz"]
>>> foo()
["baz"]

2. Using class variables incorrectly

Here's an example:


>>> class A(object):
...     x = 1
...
>>> class B(A):
...     pass
...
>>> class C(A):
...     pass
...
>>> print A.x, B.x, C.x
1 1 1

This makes sense:


>>> B.x = 2
>>> print A.x, B.x, C.x
1 2 1

One more time:

>>> A.x = 3
>>> print A.x, B.x, C.x
3 2 3

Just changed A.x, why did C.x change with it?

In Python, class variables are handled internally as dictionaries and follow the method resolution order (MRO). In the above code, because attribute x is not found in class C, it looks for its base class (only A in the above example, although Python supports multiple inheritance). In other words, C has no x attribute of its own, independent of A, so A reference to C.x is actually A reference to A.x.

3. Specify incorrect parameters for the exception

Suppose the code has the following code:


>>> try:
...     l = ["a", "b"]
...     int(l[2])
... except ValueError, IndexError:  # To catch both exceptions, right?
...     pass
...
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
IndexError: list index out of range

The problem here is that the except statement does not need this way to specify the list of exceptions. However, in Python 2.x, except Exception,e is usually used to bind the second parameter in the Exception for further checking. Therefore, in the above code, the IndexError exception is not captured by the except statement, and the exception is bound to a parameter called IndexError.

The correct way to catch multiple exceptions in an exception statement is to specify the first parameter as a tuple that contains all the exceptions that are caught. At the same time, the as keyword is used to ensure maximum portability, and both Python 2 and Python 3 support this syntax.


>>> try:
...     l = ["a", "b"]
...     int(l[2])
... except (ValueError, IndexError) as e: 
...     pass
...
>>>

4. Misunderstanding Python rule scope

Parsing is based on the scope of the Python LEGB rules, are Local, Enclosing, Global, Built - in. In fact, there are some mysteries to this method of analysis. Here is an example:


>>> x = 10
>>> def foo():
...     x += 1
...     print x
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment

Many people feel surprised when they add a parameter in the function body of work statements, will work in the previous code shall be reported to the UnboundLocalError mistakes ((link: https://docs.python.org/2/faq/programming.html#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value) to view a more detailed description).

It's easy for developers to make this mistake when using lists. Here's an example:


>>> lst = [1, 2, 3]
>>> def foo1():
...     lst.append(5)   # This works ok...
...
>>> foo1()
>>> lst
[1, 2, 3, 5] >>> lst = [1, 2, 3]
>>> def foo2():
...     lst += [5]      # ... but this bombs!
...
>>> foo2()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment

Why did foo2 fail and foo1 work?

The answer is the same as in the previous example, but with a few subtleties. Foo1 is not assigned to LST, but foo2 is. LST += [5] is actually LST = LST + [5], trying to assign a value to LST (therefore, assume that Python is in local scope). However, we are looking for the value specified to the LST to be based on the LST itself, which is not yet determined.

5. Modify the traversal list

The following code is obviously wrong:


>>> 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]  # BAD: Deleting item from a list while iterating over it
...
Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
IndexError: list index out of range

Deleting a list while traversing is a very low-level error. No one with a little experience will do it.

Modify the above code to execute correctly:


>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> numbers[:] = [n for n in numbers if not odd(n)]  # ahh, the beauty of it all
>>> numbers
[0, 2, 4, 6, 8]

6. How do I bind variables in a closure

Here's an example:


>>> def create_multipliers():
...     return [lambda x : i * x for i in range(5)]
>>> for multiplier in create_multipliers():
...     print multiplier(2)
...

What you expect:

0
2
4
6
8

In fact:

8
8
8
8
8

Very surprised! This happens mainly because of Python's late-binding behavior, where the variable is used in the closure while the inner function is calling it.

Solutions:


>>> def create_multipliers():
...     return [lambda x, i=i : i * x for i in range(5)]
...
>>> for multiplier in create_multipliers():
...     print multiplier(2)
...
0
2
4
6
8

7. Create circular module dependencies

Suppose there are two files, a.py and b.py, and then import them respectively, as follows:

In Amy polumbo y:


import b def f():
    return b.x
 
print f()

In p. y:

import a x = 1 def g():
    print a.f()

First, let's try importing a.py:

>>> import a
1

You may be surprised how well it works. After all, we did do a loop import here, shouldn't something go wrong?

The mere existence of a circular import is not a Python problem; if a module is imported, Python does not try to re-import it. At this point, each module may encounter problems at runtime when trying to access functions or variables.

What happens when we try to import b.py (not previously imported a.py) :


>>> import b
Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
     File "b.py", line 1, in <module>
    import a
     File "a.py", line 6, in <module>
 print f()
     File "a.py", line 4, in f
 return b.x
AttributeError: 'module' object has no attribute 'x'

There was an error, and the problem here is that you're also trying to import a.p while importing b.py, so you're calling f() and trying to access b.x. But b.x is not defined.

This can be solved by simply modifying the g() function that b.py imports into a.py:


x = 1
def g():
    import a # This will be evaluated only when g() is called
    print a.f()

Everything works fine whenever you import:


>>> import b
>>> b.g()
1 # Printed a first time since module 'a' calls 'print f()' at the end
1 # Printed a second time, this one is our call to 'g'

Conflict with Python standard library module name

Python has a very rich library of modules and supports "out of the box." As a result, naming conflicts can easily occur if they are not deliberately avoided. For example, you might have a module named email.py in your code that is likely to conflict with Python's native standard library module due to its consistent name.

9. The distinction between python2.x and python3.x is not handled as specified

Take a look at foo.py:


import sys def bar(i):
    if i == 1:
        raise KeyError(1)
    if i == 2:
        raise ValueError(2) def bad():
    e = None
    try:
        bar(int(sys.argv[1]))
    except KeyError as e:
        print('key error')
    except ValueError as e:
        print('value error')
    print(e) bad()

It works well in Python 2:


$ python foo.py 1
key error
1
$ python foo.py 2
value error
2

But in Python 3:

$ python3 foo.py 1
key error
Traceback (most recent call last):
  File "foo.py", line 19, in <module>
    bad()
  File "foo.py", line 17, in bad
    print(e)
UnboundLocalError: local variable 'e' referenced before assignment

Solutions:


import sys def bar(i):
    if i == 1:
        raise KeyError(1)
    if i == 2:
        raise ValueError(2) def good():
    exception = None
    try:
        bar(int(sys.argv[1]))
    except KeyError as e:
        exception = e
        print('key error')
    except ValueError as e:
        exception = e
        print('value error')
    print(exception) good()

Running results in Py3k:

$ python3 foo.py 1
key error
1
$ python3 foo.py 2
value error
2

There are many considerations and discussions in the Python recruitment guide about Python 2 and Python 3 when porting code.

Abuse the method of arbitration with arbitration

For example, here's a file called mod.py:


import foo
class Bar(object):
        ...
    def __del__(self):
        foo.cleanup(self.myhandle)

Next, you do the following in the another_mod.py file:

import mod
mybar = mod.Bar()

You get an AttributeError exception.

For more on why this exception occurs, click here. When the interpreter is closed, the global variables of the module are all set to None. Thus, in the above example, foo has all been set to None when the s/s are called.

A good solution is to use atexit.register() instead. By the way, when the program is finished, your registered handler stops working before the interpreter closes.

Fix the above problem code:


import foo
import atexit def cleanup(handle):
    foo.cleanup(handle)
class Bar(object):
    def __init__(self):
        ...
        atexit.register(cleanup, self.myhandle)

This implementation provides a clean and reliable way to invoke any functionality that needs to be cleaned up, provided that the program terminates normally.

conclusion

Python is a powerful and flexible programming language with many mechanisms and patterns to greatly improve productivity. As with any language or software tool, there is a limited understanding or appreciation of its capabilities, some of which do more harm than good, and sometimes lead to pitfalls. Understanding the nuances of a language and some of the common pitfalls can help you get further down the developer path.


Related articles: