loading...
Cover image for Good and Bad Practices of Coding in Python

Good and Bad Practices of Coding in Python

duomly profile image Duomly Updated on ・8 min read

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'

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!


Duomly - Programming Online Courses

Thank you for reading.

The article was prepared by our teammate Mirko.

Posted on by:

duomly profile

Duomly

@duomly

We believe everyone can learn how to code, so we are making learning fun and easy!

Discussion

markdown guide
 

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.

 
 

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

 

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.

 

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.

 

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.

 

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

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.

 

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!

 

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.

 

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?

 

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.

 
 

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

 
 

I feel chaining assignment to be a gun pointing at your foot. It looks nice with literals, but given that we almost always assign variables to something, I don't know...

 

Also use an auto formatter to write consistent code. I did a comparison here: kevinpeters.net/auto-formatters-fo...

 

As I understood it's all bad practices..