Good and Bad Practices of Coding in Python

Duomly - Oct 28 '19 - - Dev Community

This article was originally published at: https://www.blog.duomly.com/good-and-bad-practices-of-coding-in-python/

Python is a high-level multi-paradigm programming language that emphasizes readability. It’s being developed, maintained, and often used following the rules called The Zen of Python or PEP 20.

This article shows several examples of good and bad practices of coding in Python that you’re likely to meet often.

Using Unpacking to Write Concise Code

Packing and unpacking are powerful Python features. You can use unpacking to assign values to your variables:

>>> a, b = 2, 'my-string'
>>> a
2
>>> b
'my-string'
Enter fullscreen mode Exit fullscreen mode

You can exploit this behavior to implement probably the most concise and elegant variables swap in the entire world of computer programming:

>>> a, b = b, a
>>> a
'my-string'
>>> b
2
Enter fullscreen mode Exit fullscreen mode

That’s awesome!
Unpacking can be used for the assignment to multiple variables in more complex cases. For example, you can assign like this:

>>> x = (1, 2, 4, 8, 16)
>>> a = x[0]
>>> b = x[1]
>>> c = x[2]
>>> d = x[3]
>>> e = x[4]
>>> a, b, c, d, e
(1, 2, 4, 8, 16)
Enter fullscreen mode Exit fullscreen mode

But instead, you can use more concise and arguably more readable approach:

>>> a, b, c, d, e = x
>>> a, b, c, d, e
(1, 2, 4, 8, 16)
Enter fullscreen mode Exit fullscreen mode

That’s cool, right? But it can be even cooler:

>>> a, *y, e = x
>>> a, e, y
(1, 16, [2, 4, 8])
Enter fullscreen mode Exit fullscreen mode

The point is that the variable with * collects the values not assigned to others.

Using Chaining to Write Concise Code

Python allows you to chain the comparison operations. So, you don’t have to use and to check if two or more comparisons are True:

>>> x = 4
>>> x >= 2 and x <= 8
True
Enter fullscreen mode Exit fullscreen mode

Instead, you can write this in a more compact form, like mathematicians do:

>>> 2 <= x <= 8
True
>>> 2 <= x <= 3
False
Enter fullscreen mode Exit fullscreen mode

Python also supports chained assignments. So, if you want to assign the same value to multiple variables, you can do it in a straightforward way:

>>> x = 2
>>> y = 2
>>> z = 2
Enter fullscreen mode Exit fullscreen mode

A more elegant way is to use unpacking:

>>> x, y, z = 2, 2, 2
Enter fullscreen mode Exit fullscreen mode

However, things become even better with chained assignments:

>>> x = y = z = 2
>>> x, y, z
(2, 2, 2)
Enter fullscreen mode Exit fullscreen mode

Be careful when your value is mutable! All the variables refer to the same instance.

Checking against None

None is a special and unique object in Python. It has a similar purpose, like null in C-like languages.

It’s possible to check whether a variable refers to it with the comparison operators == and !=:

>>> x, y = 2, None
>>> x == None
False
>>> y == None
True
>>> x != None
True
>>> y != None
False
Enter fullscreen mode Exit fullscreen mode

However, a more Pythonic and desirable way is using is and is not:

>>> x is None
False
>>> y is None
True
>>> x is not None
True
>>> y is not None
False
Enter fullscreen mode Exit fullscreen mode

In addition, you should prefer using the is not construct x is not None over its less readable alternative not (x is None).

Iterating over Sequences and Mappings

You can implement iterations and for loops in Python in several ways. Python offers some built-in classes to facilitate it.

In almost all cases, you can use the range to get an iterator that yields integers:

>>> x = [1, 2, 4, 8, 16]
>>> for i in range(len(x)):
...     print(x[i])
... 
1
2
4
8
16
Enter fullscreen mode Exit fullscreen mode

However, there’s a better way to iterate over a sequence:

>>> for item in x:
...     print(item)
... 
1
2
4
8
16
Enter fullscreen mode Exit fullscreen mode

But what if you want to iterate in the reversed order? Of course, the range is an option again:

>>> for i in range(len(x)-1, -1, -1):
...     print(x[i])
... 
16
8
4
2
1
Enter fullscreen mode Exit fullscreen mode

Reversing the sequence is a more elegant way:

>>> for item in x[::-1]:
...     print(item)
... 
16
8
4
2
1
Enter fullscreen mode Exit fullscreen mode

The Pythonic way is to use reversed to get an iterator that yields the items of a sequence in the reversed order:

>>> for item in reversed(x):
...     print(item)
... 
16
8
4
2
1
Enter fullscreen mode Exit fullscreen mode

Sometimes you need both the items from a sequence and the corresponding indices:

>>> for i in range(len(x)):
...     print(i, x[i])
... 
0 1
1 2
2 4
3 8
4 16
Enter fullscreen mode Exit fullscreen mode

It’s better to use enumerate to get another iterator that yields the tuples with the indices and items:

>>> for i, item in enumerate(x):
...     print(i, item)
... 
0 1
1 2
2 4
3 8
4 16
Enter fullscreen mode Exit fullscreen mode

That’s cool. But what if you want to iterate over two or more sequences? Of course, you can use the range again:

>>> y = 'abcde'
>>> for i in range(len(x)):
...     print(x[i], y[i])
... 
1 a
2 b
4 c
8 d
16 e
Enter fullscreen mode Exit fullscreen mode

In this case, Python also offers a better solution. You can apply zip and get tuples of the corresponding items:

>>> for item in zip(x, y):
...     print(item)
... 
(1, 'a')
(2, 'b')
(4, 'c')
(8, 'd')
(16, 'e')
Enter fullscreen mode Exit fullscreen mode

You can combine it with unpacking:

>>> for x_item, y_item in zip(x, y):
...     print(x_item, y_item)
... 
1 a
2 b
4 c
8 d
16 e
Enter fullscreen mode Exit fullscreen mode

Please, have in mind that range can be very useful. However, there are cases (like those shown above) where there are more convenient alternatives.
Iterating over a dictionary yields its keys:

>>> z = {'a': 0, 'b': 1}
>>> for k in z:
... print(k, z[k])
... 
a 0
b 1
Enter fullscreen mode Exit fullscreen mode

However, you can apply the method .items() and get the tuples with the keys and the corresponding values:

>>> for k, v in z.items():
...     print(k, v)
... 
a 0
b 1
Enter fullscreen mode Exit fullscreen mode

You can also use the methods .keys() and .values() to iterate over the keys and values, respectively.

Comparing to Zero

When you have numeric data, and you need to check if the numbers are equal to zero, you can but don’t have to use the comparison operators == and !=:

>>> x = (1, 2, 0, 3, 0, 4)
>>> for item in x:
...     if item != 0:
...         print(item)
... 
1
2
3
4
Enter fullscreen mode Exit fullscreen mode

The Pythonic way is to exploit the fact that zero is interpreted as False in a Boolean context, while all other numbers are considered as True:

>>> bool(0)
False
>>> bool(-1), bool(1), bool(20), bool(28.4)
(True, True, True, True)
Enter fullscreen mode Exit fullscreen mode

Having this in mind you can just use if item instead of if item != 0:

>>> for item in x:
...     if item:
...         print(item)
... 
1
2
3
4
Enter fullscreen mode Exit fullscreen mode

You can follow the same logic and use if not item instead of if item == 0.

Avoiding Mutable Optional Arguments

Python has a very flexible system of providing arguments to functions and methods. Optional arguments are a part of this offer. But be careful: you usually don’t want to use mutable optional arguments. Consider the following example:

>>> def f(value, seq=[]):
...     seq.append(value)
...     return seq
Enter fullscreen mode Exit fullscreen mode

At first sight, it looks like that, if you don’t provide seq, f() appends a value to an empty list and returns something like [value]:

>>> f(value=2)
[2]
Enter fullscreen mode Exit fullscreen mode

Looks fine, right? No! Consider the following examples:

>>> f(value=4)
[2, 4]
>>> f(value=8)
[2, 4, 8]
>>> f(value=16)
[2, 4, 8, 16]
Enter fullscreen mode Exit fullscreen mode

Surprised? Confused? If you are, you’re not the only one.
It seems that the same instance of an optional argument (list in this case) is provided every time the function is called. Maybe sometimes you’ll want just what the code above does. However, it’s much more likely that you’ll need to avoid that. You can keep away from that with some additional logic. One of the ways is this:

>>> def f(value, seq=None):
...     if seq is None:
...         seq = []
...     seq.append(value)
...     return seq
Enter fullscreen mode Exit fullscreen mode

A shorter version is:

>>> def f(value, seq=None):
...     if not seq:
...         seq = []
...     seq.append(value)
...     return seq
Enter fullscreen mode Exit fullscreen mode

Now, you get different behavior:

>>> f(value=2)
[2]
>>> f(value=4)
[4]
>>> f(value=8)
[8]
>>> f(value=16)
[16]
Enter fullscreen mode Exit fullscreen mode

In most cases, that’s what one wants.

Avoiding Classical Getters and Setters

Python allows defining getter and setter methods similarly as C++ and Java:

>>> class C:
...     def get_x(self):
...         return self.__x
...     def set_x(self, value):
...         self.__x = value
Enter fullscreen mode Exit fullscreen mode

This is how you can use them to get and set the state of an object:

>>> c = C()
>>> c.set_x(2)
>>> c.get_x()
2
Enter fullscreen mode Exit fullscreen mode

In some cases, this is the best way to get the job done. However, it’s often more elegant to define and use properties, especially in simple cases:

>>> class C:
...     @property
...     def x(self):
...         return self.__x
...     @x.setter
...     def x(self, value):
...         self.__x = value
Enter fullscreen mode Exit fullscreen mode

Properties are considered more Pythonic than classical getters and setters. You can use them similarly as in C#, i.e. the same way as ordinary data attributes:

>>> c = C()
>>> c.x = 2
>>> c.x
2
Enter fullscreen mode Exit fullscreen mode

So, in general, it’s a good practice to use properties when you can and C++-like getters and setters when you have to.

Avoiding Accessing Protected Class Members

Python doesn’t have real private class members. However, there’s a convention that says that you shouldn’t access or modify the members beginning with the underscore (_) outside their instances. They are not guaranteed to preserve the existing behavior.

For example, consider the code:

>>> class C:
...     def __init__(self, *args):
...         self.x, self._y, self.__z = args
... 
>>> c = C(1, 2, 4)
Enter fullscreen mode Exit fullscreen mode

The instances of class C have three data members: .x, .y, and ._Cz. If a member’s name begins with a double underscore (dunder), it becomes mangled, that is modified. That’s why you have ._Cz instead of ._z.
Now, it’s quite OK to access or modify .x directly:

>>> c.x  # OK
1
Enter fullscreen mode Exit fullscreen mode

You can also access or modify ._y from outside its instance, but it’s considered a bad practice:

>>> c._y  # Possible, but a bad practice!
2
Enter fullscreen mode Exit fullscreen mode

You can’t access .z because it’s mangled, but you can access or modify ._Cz:

>>> c.__z # Error!
Traceback (most recent call last):
File "", line 1, in 
AttributeError: 'C' object has no attribute '__z'
>>> c._C__z # Possible, but even worse!
4
>>>
Enter fullscreen mode Exit fullscreen mode

You should avoid doing this. The author of the class probably begins the names with the underscore(s) to tell you, “don’t use it”.

Using Context Managers to Release Resources

Sometimes it’s required to write the code to manage resources properly. It’s often the case when working with files, database connections, or other entities with unmanaged resources. For example, you can open a file and process it:

>>> my_file = open('filename.csv', 'w')
>>> # do something with `my_file`
Enter fullscreen mode Exit fullscreen mode

To properly manage the memory, you need to close this file after finishing the job:

>>> my_file = open('filename.csv', 'w')
>>> # do something with `my_file and`
>>> my_file.close()
Enter fullscreen mode Exit fullscreen mode

Doing it this way is better than not doing it at all. But, what if an exception occurs while processing your file? Then my_file.close() is never executed. You can handle this with exception-handling syntax or with context managers. The second way means that you put your code inside the with a block:

>>> with open('filename.csv', 'w') as my_file:
...     # do something with `my_file`
Enter fullscreen mode Exit fullscreen mode

Using the with block means that the special methods .enter() and .exit() are called, even in the cases of exceptions. These methods should take care of the resources.
You can achieve especially robust constructs by combining the context managers and exception handling.

Stylistic Advises

Python code should be elegant, concise, and readable. It should be beautiful.

The ultimate resource on how to write beautiful Python code is Style Guide for Python Code or PEP 8. You should definitely read it if you want to code in Python.

Conclusions

This article gives several advises on how to write a more efficient, more readable, and more concise code. In short, it shows how to write a Pythonic code. In addition, PEP 8 provides the style guide for Python code, and PEP 20 represents the principles of Python language.

Enjoy writing Pythonic, useful, and beautiful code!


Duomly - Programming Online Courses

Thank you for reading.

The article was prepared by our teammate Mirko.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .