DEV Community

Cover image for Good and Bad Practices of Coding in Python
Duomly
Duomly

Posted on • Edited on • Originally published at blog.duomly.com

Good and Bad Practices of Coding in Python

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.

Top comments (19)

Collapse
 
idanarye profile image
Idan Arye

Please don't treat numbers as booleans. It makes sense when you are checking for None or for emptiness, but in case of numbers it's just confusing. That's an artifact of the C heritage all languages and programmers have nowadays, but that doesn't mean we should be using it.

Collapse
 
arnauddupuis profile image
Arnaud Dupuis

Agreed!

Collapse
 
tariqabughofa profile image
Tariq Abughofa

I totally agree but the sad thing is that many of these practices are considered "pythonic".

Collapse
 
schneebuzz profile image
schneebuzz • Edited

Nice writing.

For the optional argument check it's also possible to do

seq = seq or []

so if it's None it will assign an empty list or use seq otherwise.
Or are there any downsides with this approach?
E: Ok in case seq is already an empty list it will be reassigned anyway.

Collapse
 
herobank110 profile image
David Kanekanian

Checking an optional argument evaluates to True will not do the same as explicitly checking it is not None. If an empty list was passed as an argument it would still evaluate to False and a new list would be made.

Collapse
 
philipstarkey profile image
Phil Starkey

Was going to say the same thing! That part of the article is definitely poor advice, especially for newcomers who are likely not already be confused by the behaviour.

Collapse
 
sobolevn profile image
Nikita Sobolev

Great guide! It covers so many common problems!

And almost all of these checks can be automated with wemake-python-styleguide: it is a linter for python that can catch common mistakes and bad code, including stylistic and semantic issues.

Check it out!

GitHub logo wemake-services / wemake-python-styleguide

The strictest and most opinionated python linter ever!

wemake-python-styleguide

wemake.services Supporters Build Status codecov Python Version wemake-python-styleguide


Welcome to the strictest and most opinionated python linter ever.

wemake-python-styleguide logo

wemake-python-styleguide is actually a flake8 plugin with some other plugins as dependencies.

Quickstart

pip install wemake-python-styleguide

You will also need to create a setup.cfg file with the configuration.

We highly recommend to also use:

  • flakehell for easy integration into a legacy codebase
  • nitpick for sharing and validating configuration across multiple projects

Running

flake8 your_module.py

This app is still just good old flake8 And it won't change your existing workflow.

invocation results

See "Usage" section in the docs for examples and integrations.

We also support GitHub Actions as first class-citizens Try it out!

What we are about

The ultimate goal of this project is to make all people write exactly the same python code.

flake8 pylint black mypy wemake-python-styleguide
Formats code?
Finds style issues? 🤔 🤔
Finds bugs? 🤔
Collapse
 
paddy3118 profile image
Paddy3118

The Pythonic way is to exploit the fact that zero is interpreted as False in a Boolean context,

If the test is for conceptually numeric zero then best to test for the number; especially if there are allied tests for different numbers adjacent.

Collapse
 
laurakateholley profile image
LauraHolley

I'm new to Python so appreciated the advice and tips in this article. I also appreciate the comments offering alternatives and opposing a couple of the points! Shows there are different approaches to writing "Pythonic" code.
Thanks!

Collapse
 
arnauddupuis profile image
Arnaud Dupuis

The late binding thing is clearly a bug. I don't understand how the python community consider that a feature.
It look to me that some people thought of optimizing the computing speed of variables allocation and decided it was shorter to reference the same heap space instead of doing a new stack allocation. It is indeed faster but leads to that obviously flawed behavior.
Now I'm not developing the interpreter so I might be totally wrong but so far I haven't seen a valid justification to that behavior.

Collapse
 
idanarye profile image
Idan Arye

What do you mean by "late binding"? AFAIK late binding is a slightly different term (with slightly different semantics) for dynamic typing, but from the way you talk about this it seems you are referring to the default arguments gotcha?

Collapse
 
thebouv profile image
Anthony Bouvier

I want to like this.

But I don't.

Because it is basically an advertisement for a non-sponsor of DEV.to (afaict).

So it feels bad. I don't trust a single heart, unicorn, or whatever on this article.

Collapse
 
omwakwe profile image
Starford Omwakwe • Edited

Very cool tips

Collapse
 
duomly profile image
Duomly

Happy you like it :)

Collapse
 
iioaia profile image
iioaia • Edited

After a few paragraphs I've understood a bit more about python. I'm new to programming, thanks for sharing!