Skip to content

Python Bad Practices

| python |

I’ve been using Python as scripting language on a non-regular basis. It means I can (I possibly do) write non-idiomatic Python code. I’ve decided to collect in one place popular Python anti-patterns and bad practices to avoid them.


References

Original sources

  1. Python Patterns (see src)
  2. The Little Book of Python Anti-Patterns (see src)
  3. Python Data Structures Idioms by Ivan Mushketyk
  4. Buggy Python Code: The 10 Most Common Mistakes That Python Developers Make by Martin Chikilian
  5. Youtube: 5 Common Python Mistakes and How to Fix Them by Corey Schafer
  6. Python Worst Practices by Daniel Greenfeld
  7. What the f*ck Python! 😱

Python standard library


1 - Collections

1.1 - Iterate over a list

Ref: [3]

Very Bad

l = [1, 2, 3, 4, 5]
i = 0
while i < len(l):
    print(l[i])
    i += 1

Bad

for i in range(len(l)):
    print(l[i])

Good

for v in l:
    print(v)

1.2 - Iterate over a list in reverse order

Ref: [3]

Bad

for i in range(len(l) - 1, -1, -1):
    print(l[i])

Good

for i in reversed(l):
    print(i)

1.3 - Access the last element in a list

Ref: [3]

Bad

l = [1, 2, 3, 4, 5]
>>> l[len(l) - 1]
5

Good

>>> l[-1]
5
>>> l[-2]
4
>>> l[-3]
3

1.4 - Use sequence unpacking

Ref: [3]

Bad

l1 = l[0]
l2 = l[1]
l3 = l[2]

Good

l1, l2, l3 = [1, 2, 3]

>>> l1
1
>>> l2
2
>>> l3
3

1.5 - Use lists comprehensions

Ref: [3]

Bad

under_18_grades = []
for grade in grades:
    if grade.age <= 18:
        under_18_grades.append(grade)

Good

under_18_grades = [grade for grade in grades if grade.age <= 18]

1.6 - Use enumerate function

Ref: [3, 6]

Sample 1

Bad

for i in range(len(menu_items)):
    menu_items = menu_items[i]
    print("{}. {}".format(i, menu_items))

Good

for i, menu_items in enumerate(menu_items):
    print("{}. {}".format(i, menu_items))

Sample 2

Bad

foo = [1, 2, 3]
for i, item in zip(range(len(foo)), foo):
    print i, item

Good

foo = [1, 2, 3]
for i, item in enumerate(foo):
    print i, item

1.7 - Use keys to sort list

Ref: [3]

people = [Person('John', 30), Person('Peter', 28), Person('Joe', 42)]

Bad

def compare_people(p1, p2):
    if p1.age < p2.age:
        return -1
    if p1.age > p2.age:
        return 1
    return 0

sorted(people, cmp=compare_people)

[Person(name='Peter', age=28), Person(name='John', age=30), Person(name='Joe', age=42)]

Good

sorted(people, key=lambda p: p.age)

[Person(name='Peter', age=28), Person(name='John', age=30), Person(name='Joe', age=42)]

1.8 - Use all/any functions

Ref: [3]

Bad

def all_true(lst):
    for v in lst:
        if not v:
            return False
    return True

Good

all([True, False])
>> False

any([True, False])
>> True
all([person.age > 18 for person in people])

# or generator

all(person.age > 18 for person in people)

1.9 - Dictionaries: avoid using keys() function

Ref: [3]

Bad

for k in d.keys():
    print(k)

Good

for k in d:
    print(k)

1.10 - Dictionaries: Iterate over keys and values

Ref: [3]

Bad

for k in d:
    v = d[k]
    print(k, v)

Good

for k, v in d.items():
    print(k, v)

1.11 - Use dictionaries comprehension

Ref: [3]

Bad

d = {}
for person in people:
    d[person.name] = person

Good

d = {person.name: person for person in people}

1.12 - Use namedtuple instead of simple class

Ref: [3]

Bad

class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

Good

from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])

In addition namedtuple implements __str__, __repr__, and __eq__ methods:

>>> Point(1, 2)
Point(x=1, y=2)
>>> Point(1, 2) == Point(1, 2)
True

1.13 - Use defaultdict and/or Counter

Ref: [3]

We need to count a number of times an element is encountered in a collection

Bad

d = {}
for v in lst:
    if v not in d:
        d[v] = 1
    else:
        d[v] += 1

Good

>>> d = defaultdict(lambda: 42)
>>> d['key']
42
from collections import defaultdict
d = defaultdict(int)
for v in lst:
    d[v] += 1

Good

from collections import Counter

>>> counter = Counter(lst)
>>> counter
Counter({4: 3, 1: 2, 2: 1, 3: 1, 5: 1})

1.14 - Modifying a list while iterating over it

Ref: [4]

Bad

>>> 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

Good

>>> 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]

2 - Functions

2.1 - Mutable Default Args

Ref: [4, 5]

Bad

def foo(bar=[]): # bar is optional and defaults to [] if not specified
    bar.append("baz")
    return bar
>>> foo()
["baz"]
>>> foo()
["baz", "baz"]
>>> foo()
["baz", "baz", "baz"]

The default value for a function argument is only evaluated once, at the time that the function is defined. Thus, the bar argument is initialized to its default (i.e., an empty list) only when foo() is first defined, but then calls to foo() (i.e., without a bar argument specified) will continue to use the same list to which bar was originally initialized.

Good

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.2 - Generally using lambdas

Ref: [6]

Bad

  • Too many characters on one line
  • Lambdas by design does not have docstrings
  • Does not necessarily mean less characters
  • I can’t get this sample to work!
swap = lambda a, x, y:
        lambda f = a.__setitem__:
        (f(x, (a[x], a[y])),
        f(y, a[x][0]), f(x, a[x][1]))()

Good

  • Doc strings that show up nicely in help/Sphinx
  • Easier to read
  • In Python, functions are first class objects
  • Whenever possible avoid using lambdas
def swap(a, x, y):
    """ Swap two position values in a list """
    a[x],a[y] = a[y],a[x]

3 - Variables

3.1 - Misunderstanding Python scope rules

Ref:

Sample 1

Bad

def func1(param=None):
    def func2():
        if not param:
            param = 'default'
        print param
    # Just return func2.
    return func2


if __name__ == '__main__':
   func1('test')()
$ python test.py 
Traceback (most recent call last):
  File "test.py", line 11, in 
    func1('test')()
  File "test.py", line 3, in func2
    if not param:
UnboundLocalError: local variable 'param' referenced before assignment

Good

def func1(param=None):
    def func2(param2=param):
        if not param2:
            param2 = 'default'
        print param2
    # Just return func2.
    return func2

Sample 2

Bad

>>> 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
>>> 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

3.2 - Confusing how Python binds variables in closures

Ref: [4]

Bad

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

Expected

0
2
4
6
8

Actual

8
8
8
8
8

Good

>>> 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

3.3 - Variable naming

Ref: [6]

Bad

object = MyObject()
map = Map()
zip = 90213 # common US developer mistake
id = 34 # I still fight this one

Good

obj = MyObject() # necessary abbreviation
object_ = MyObject() # Underscore so we don't overwrite

map_obj = Map() # combine name w/necessary abbreviation
map_ = Map()

zip_code = 90213 # Explicit name with US focus
postal_code = 90213 # i18n explicit name
zip_ = 90213

pk = 34 # pk is often synonymous with id
id_ = 34

3.4 - Identifying variable types with prefixes

Ref: [6]

Bad

c = "green"
a = False
p = 20
t = "04/20/2011"
clr = "green"
ctv = False
pythnYrs = 20
pthnFrstSd = "04/20/2011"
strColor = "green"
boolActive = False
intPythonYears = 20
dtPythonFirstUsed = "04/20/2011"

Good

color = "green"
active = False
python_years = 20
python_first_used = "04/20/2011"

4 - Classes

4.1 - Implementing Java-style getters and setters

Ref: [6]

Bad

import logging
log = logging.getLogger()

class JavaStyle:
    """ Quiz: what else am I doing wrong here? """

    def __init__(self):
        self.name = ""

    def get_name(self):
        return self.name

    def set_name(self, name):
        log.debug("Setting the name to %s" % name)
        if isinstance(name, str):
            self.name = name
        else:
            raise TypeError()

    if __name__ == "__main__":
        j = JavaStyle()
        j.set_name("pydanny did this back in 2006!")
        print(j.get_name())

Good

import logging
log = logging.getLogger()

class PythonStyle(object):

    def __init__(self):
        self._name = ""

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        """ Because name is probably a string we'll assume that we can
            infer the type from the variable name"""
        log.debug("Setting the name to %s" % value)
        self._name = value

    if __name__ == "__main__":
        p = PythonStyle()
        p.name = "pydanny doing it the right way"
        print(p.name)

4.2 - Using property setters as action methods

Ref: [6]

Bad

class WebService:

    @property
    def connect(self):
        self.proxy = xmlrpc.Server("http://service.xml")

if __name__ == '__main__':
    ws = WebService()
    ws.connect

Good

class WebService:

    def connect(self):
        self.proxy = xmlrpc.Server("http://service.xml")

if __name__ == '__main__':
    ws = WebService()
    ws.connect()

5 - Exceptions

5.1 - Passing Generic Exceptions silently

Ref: [6]

Bad

try:
    do_akshun(value)
except:
    pass

Good

Use specific exceptions and/or logging

class AkshunDoesNotDo(Exception):
    """ Custom exceptions makes for maintainable code """
    pass

try:
    do_akshun(value)
except AttributeError as e:
    log.info("Can I get attribution for these slides?")
    do_bakup_akshun(vlue)
except Exception as e:
    log.debug(str(e))
    raise AkshunDoesNotDo(e)