DEV Community

Cover image for Writing clean code in Python
Cédric Teyton for Packmind

Posted on • Updated on • Originally published at packmind.com

Writing clean code in Python

Python is a programming language that offers a high level of flexibility. The counterpart is that developers can easily use different tricks that will lead to heterogeneity in the source code, decreasing its readability and maintainability. As with any programming language, it’s important to define best practices in a team to bring consistency to the source code, avoid bugs, and save time during code reviews.

At Packmind, we recently ran a webinar with our partner Arolla (the replay is in French) on How to write clean code in Python? We share in this post an extract of the discussed practices.

NB: Please note that we don’t claim the following practices are always valid, and the “don’t” examples are always bad. Trust yourself ;)

#1 Use Counter to count occurrences

Using the Counter from the collection library is more efficient at run time when you want to count different occurrences of elements in a list, tuple, or another hashable iterable:

from collections import Counter
array = [1, 1, 2, 3, 4, 5, 3, 2, 3, 4, 2, 1, 2, 3]
counts = Counter(array)
print(counts)
# Will print => Counter({2: 5, 3: 4, 1: 3, 4: 2, 5: 1})
Enter fullscreen mode Exit fullscreen mode

#2 Use "in" to simplify if statements

The keyword “in” is an elegant, readable and maintainable way to check the presence of a specific element in a sequence :

detectives = ["Sherlock Holmes", "Hercule Poirot", "Batman"]
person = "Batman"

# Don't
if person == "Batman" or person == "Hercule Poirot" or person == "Sherlock Holmes":
    print("That person is a detective")

# Do 
if person in detectives:
    print("That person is a detective")
Enter fullscreen mode Exit fullscreen mode

#3 Put actual before expected in assertions

Assertions will be easier to read in this order:

def test_big_stuff():
    actual_result = ...
    expected_result = ...

    assert actual_result == expected_result
Enter fullscreen mode Exit fullscreen mode

#4 Use properties when relevant

Sometimes, when we create a class, we will have a field whose value stems from one or multiple other ones. For example, in a class Person, we can have a full_name field which concatenates the values of first_name and last_name .

In such cases, it is important to protect the content of the composite field by defining a property with the annotation @property. Going back to our example with the class Person, this will prevent a user to set the value of the full_name from outsite by writing person.full_name = ... .

class Person:
      def __init__(self, first_name, last_name):
            self.first_name = first_name
            self.last_name = last_name

      @property
      def full_name(self):
          return f"{self.first_name} {self.last_name}"
Enter fullscreen mode Exit fullscreen mode

#5 Use fully qualified, absolute imports

This makes the code more readable and maintainable, so that when it’s time to modify the code, it is easier to figure where each object in the code comes from.

Performance-wise, it is basically the same as importing the full module (ex. import foo ) as Python always loads the full module, whether or not we import just an object of that module.

That is to say if when we write from foo.bar import Bar , Python loads the entirety of the module foo.bar and then proceeds to pick Bar

from foo.bar import Bar
from spam.eggs import Eggs

def main():
    bar = Bar()
    eggs = Eggs()
Enter fullscreen mode Exit fullscreen mode

#6 Use iterators instead of explicit lists

Avoid creating a new list when it’s not relevant.

def get_max():
    iterable = ["a", "bbb", "c"]
    # Don't
    max_len = max([len(x) for x in iterable])
    # Do
      max_len = max(len(x) for x in iterable)
    assert max_len == 3

get_max()
Enter fullscreen mode Exit fullscreen mode

#7 Use list comprehensions

A list comprehension is a way of creating a new list by transforming elements from an existing iterable (such as a list, tuple, or dictionary), and we want to, filter some elements, and perform operations on each element.

# Don't
def get_even_nums_squared():
    nums = [1, 2, 3, 4, 5, 6]
    res = []
    for num in nums:
        if num % 2 == 0:
            res.append(num * num)
    return res

# Do
def get_even_nums_squared():
    nums = [1, 2, 3, 4, 5, 6]
    return [x * x for x in nums if x % 2 == 0]
Enter fullscreen mode Exit fullscreen mode

#8 Prefer using keyword-only arguments

Many times, especially when there is no logical order between the parameters of a function or method, it is recommended to call the function or method by specifying the name of the parameters (ex. make_coffee(with_sugar=True, with_milk=True) ).

It is possible to force the parameters to be named when the function/method is called. We can do that by using the “*” at the beginning of the parameters.

This avoids many possible issues and confusion.

However, it is not something to do all the time but rather when it makes sense.

Instead of:

def make_coffee(with_sugar=False, with_milk=False):
    pass

make_coffee(True, True)
Enter fullscreen mode Exit fullscreen mode

We’d prefer:

def make_coffee(*, with_sugar=False, with_milk=False):
    pass

make_coffee(with_milk=True, with_sugar=True)
Enter fullscreen mode Exit fullscreen mode

#9 Use ABCMeta for abstract classes

This practice can be relevant if you work with developers who are not expert in Python, but are more familiar with Java or C#. They’ll be more comfortable with the “abstract” concepts for classes and methods.

ABCMeta is a metaclass (a class that creates classes) in Python. It stands for "Abstract Base Class Meta".

Instead of:

class Fooer:
    def foo(self):
        raise NotImplementedError() 

class Spam(Fooer):
    def foo(self):
        print("spamming")
Enter fullscreen mode Exit fullscreen mode

We’d prefer:

from abc import ABCMeta, abstractmethod

class Fooer(metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass

class Spam(Fooer):
    def foo(self):
        print("spamming foos")
Enter fullscreen mode Exit fullscreen mode

#10 Use a main() function

Avoid global variables and, in general, source code outside functions.

Instead of:

from server import Server

HOST = "127.0.0.1"
PORT = 8080

SERVER = Server()

if __name__ == "__main__":
    SERVER.start(HOST, PORT)
Enter fullscreen mode Exit fullscreen mode

We’d prefer:

from server import Server

def main():
    host  = "127.0.0.1"
    port = 8080

    Server = Server()
    Server.start(host, port)

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

#11 Do not use empty lists as default arguments

This can lead to unexpected and very weird behavior.

Instead of:

def add_player_to_team(player, team=[]):
    team.append(player)
    print(team)
Enter fullscreen mode Exit fullscreen mode

We’d prefer::

def add_player_to_team(player, team=None):
    if team is None:
        team = []

    team.append(player)
    print(team)
Enter fullscreen mode Exit fullscreen mode

#12 Prefer f-strings to string concatenation

F-strings allow writing sentences in a far more natural (and admittedly less annoying) way than what string concatenation can provide.

Do:

first_name = "Jake"
last_name = "Sully"
age = 28

message = f"{first_name} {last_name} is {age} years old now"
print(message)
Enter fullscreen mode Exit fullscreen mode

Don’t:

first_name = "Jake"
last_name = "Sully"
age = 28

message = first_name + " " + last_name + " is " + str(age) + " years old now"
print(message)
Enter fullscreen mode Exit fullscreen mode

#13 Prefer enumerate() to range(len()) when you want keep the index of iterable items

This practice contributes to code readability as well as its performance while still keeping the index around. The performance gain is because enumerate() creates an iterator for the collection, which is more efficient than looping through each item

Don’t:

ages = [1, 2, 18, 24, 8]
for i in range(len(ages)):
    if ages[i] >= 18:
        print(f"I'm client n°{i+1} and I'm {age} years old, I'm an adult now.")
Enter fullscreen mode Exit fullscreen mode

Do:

ages = [1, 2, 18, 24, 8]
for i, age in enumerate(ages):
    if age >= 18:
        print(f"I'm client n°{i+1} and I'm {age} years old, I'm an adult now.")
Enter fullscreen mode Exit fullscreen mode

All these best practices can be defined in Packmind from our IDE and Code reviews plugins. We’re compatible with VSCode, JetBrains suite, and Eclipse. So if you code Python with VSCode or PyCharm, you can go for it! Each practice you create will then be validated as a team during dedicated workshops, and the final result looks like this:

An example of best practice in Packmind

You can provide syntactic patterns to provide suggestions while coding or reviewing code and use this practice during onboarding workshops in Packmind.

The whole catalog is also available on our public Hub of best practices, where users can share practices on various domains and use them in Packmind.

You can start creating your practices now for free.

Top comments (2)

Collapse
 
fayomihorace profile image
Horace FAYOMI

This is a crazy good article for people who want write nice python code.
But please, why the points #10 Use a main() function and #11 Do not use empty lists as default arguments, I mean which issue might occurs if we do themand in which circumstances ?

Collapse
 
dmerejkowsky profile image
Dimitri Merejkowsky • Edited

Regarding the main() function:

  • having a main() function gives a scopes for lots of variables which would otherwise be globals - and having less globals is often a win ;)
  • having a separate main() function makes it easy to import your module to do something else with it (like write some tests)

Regarding the default arguments : it's all about the principle of least astonishment see :
this nice answer on stack overflow

PS:

This is a crazy good article

Thanks. This is very much appreciated :)