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'
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
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)
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)
That’s cool, right? But it can be even cooler:
>>> a, *y, e = x
>>> a, e, y
(1, 16, [2, 4, 8])
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
Instead, you can write this in a more compact form, like mathematicians do:
>>> 2 <= x <= 8
True
>>> 2 <= x <= 3
False
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
A more elegant way is to use unpacking:
>>> x, y, z = 2, 2, 2
However, things become even better with chained assignments:
>>> x = y = z = 2
>>> x, y, z
(2, 2, 2)
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
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
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
However, there’s a better way to iterate over a sequence:
>>> for item in x:
... print(item)
...
1
2
4
8
16
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
Reversing the sequence is a more elegant way:
>>> for item in x[::-1]:
... print(item)
...
16
8
4
2
1
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
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
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
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
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')
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
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
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
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
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)
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
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
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]
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]
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
A shorter version is:
>>> def f(value, seq=None):
... if not seq:
... seq = []
... seq.append(value)
... return seq
Now, you get different behavior:
>>> f(value=2)
[2]
>>> f(value=4)
[4]
>>> f(value=8)
[8]
>>> f(value=16)
[16]
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
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
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
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
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)
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
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
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
>>>
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`
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()
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`
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!
Thank you for reading.
The article was prepared by our teammate Mirko.