Make your Python code more readable and efficient


Python is a general-purpose programming language widely used in scientific computing, artificial intelligence, web development, financial modeling, and many other fields. The main reason for its popularity is flexibility - there are many solutions for various kinds of operations. However, in most cases there is only one solution that is considered preferred among experienced Python programmers. In this article, I would like to review 10 typical practical examples of this language, which can be evaluated and adopted for refactoring Python code.

1. Inclusion (approx. Transl.: abstractions) of lists, dictionaries, and sets

There are three typical mutable Python container data types: lists, dictionaries, and sets. If we have an iterable object, then we can use the for loop to iterate over this object to create a new list based on it. However, the Python-specific way is to use the inclusion of a list, the syntax of which is as follows: [expression for x in iterable_object if condition]. It should be noted that the part with the calculated condition is optional. Let's look at the following code:

>>> # Начнем с создания списка чисел >>> numbers=list(range(5)) >>> print(numbers) [0, 1, 2, 3, 4] >>> >>> # Вместо этого: >>> squares0=[] >>> for number in numbers: ... if number%2 == 0: ... squares0.append(number*number) ... >>> print(squares0) [0, 4, 16] >>> >>> # Характерный для Python способ: >>> squares1=[x*x for x in numbers if x%2 == 0] >>> print(squares1) [0, 4, 16] 

List inclusion

In addition to including the list, we can also use inclusions for dictionaries and sets, which corresponds to the terms "inclusion of the dictionary" and "inclusion of the set." To include a dictionary, the following syntax is used: {key_expression: value_expression for element in iterable_object}, while the syntax for including a set is {expression for element in in iterable_obj}. This code demonstrates their use:

>>> # Включение словаря >>> {x: x*x for x in range(5)} {0: 0, 1: 1, 2: 4, 3: 9, 4: 16} >>> >>> # Включение множества >>> {x*x for x in range(5)} {0, 1, 4, 9, 16} 

Dictionary and set inclusions


Strings are a familiar primitive data type that we use in almost all projects. In most cases, to display string data, we need to take an additional step - to format them in a certain way. You can perform formatting in the style of the C language, which implies the use of the% character, or use the format method that strings have in Python.

However, in a recent Python release, a new string formatting method was introduced. It is known as “f-strings,” which means “formatted string literals,” a concise and readable way to format strings. Let's take a look at comparing these different formatting approaches

>>> # Определим коэффициент конвертации >>> usd_to_eur=0.89 >>> >>> # Вместо этого: >>> print('1 USD={rate:.2f} EUR'.format(rate=usd_to_eur)) 1 USD=0.89 EUR >>> print('1 USD=%.2f EUR' % usd_to_eur) 1 USD=0.89 EUR >>> >>> # Характерный для Python способ: >>> print(f'1 USD={usd_to_eur:.2f} EUR') 1 USD=0.89 EUR 


Naturally, the above code shows only the basics of using f-lines, which in essence implement almost all formatting styles supported by C-style formatting or the str.format method. You can read more about f-lines in the official documentation or in my recent article.

3. Multiple Tuple Assignment and Unpacking

When working with variables, defining one variable on one line is a well-established practice. However, when declaring multiple variables, we can do this on one line. To avoid confusion, please note that this way you can declare variables that are related in meaning. If they are used for different purposes, then I would not recommend doing their multiple assignment. Let's take a look at refactoring:

>>> # Вместо этого: >>> code=404 >>> message="Not Found" >>> >>> # Характерный для Python способ: >>> code, message=404, "Not Found" 

Multiple Assignment

Multiple assignment behind the scenes involves creating a tuple on the right side and unpacking it on the left side. The following code shows how to unpack a tuple. As you can see, this is reminiscent of multiple assignment because it uses the same principle.

>>> # Определяем и распаковываем кортеж >>> http_response=(404, "Not Found") >>> code, message=http_response >>> print(f'code: {code}; message: {message}') code: 404; message: Not Found 

Unpacking the tuple

4.Unpacking with traps

In the previous section, we examined how to unpack a tuple using the most basic format - the number of variables corresponded to the number of elements in the tuple. However, when there are several elements in a tuple, sometimes it may be necessary to unpack using the trap method (approx. Transl. that is, this is not a generally accepted term, because it has not been generally accepted, but this is the best that came to mind). Namely, all elements that are not explicitly indicated by variables will be captured by a variable with an asterisk prefix. To get the same result, an approach uncharacteristic for Python usually involves the use of slices, which is fraught with errors if we specify incorrect indexes.

>>> # Определяем кортеж для распаковки >>> numbers=tuple(range(8)) >>> numbers (0, 1, 2, 3, 4, 5, 6, 7) >>> >>> # Вместо этого: >>> first_number0=numbers[0] >>> middle_numbers0=numbers[1:-1] >>> last_number0=numbers[-1] >>> >>> # Характерный для Python способ: >>> first_number1, *middle_numbers1, last_number1=numbers >>> >>> # Проверяем результаты >>> print(first_number0 == first_number1) True >>> print(middle_numbers0 == middle_numbers1) False >>> print(last_number0 == last_number0) True 

Unpacking with traps

As you may have noticed, the values ​​of middle_numbers0 and middle_numbers1 are not equal. This is because unpacking with traps (using an asterisk) by default generates a list object. So that the decompressed data output has the same data type, we can use the tuple constructor, as shown below:

>>> # Конвертируем распакованный список в кортеж >>> print(middle_numbers0 == tuple(middle_numbers1)) True 

Creating a tuple from a trap variable

5. Assignment Expression

The assignment expression, better known as the walrus expression, uses the walrus operator: =, which resembles a walrus with a pair of eyes and tusks, isn't it? As its name implies, an assignment expression allows you to assign a value to a variable and, at the same time, it can be used as an expression, such as a conditional statement. The definition sounds confusing, but let's look at its use in the following code snippet:

>>> # Определим некоторые вспомогательные функции >>> def get_account(social_security_number): ... pass ... >>> def withdraw_money(account_number): ... pass ... >>> def found_no_account(): ... pass ... >>> # Вместо этого: >>> account_number=get_account("123-45-6789") >>> if account_number: ... withdraw_money(account_number) ... else: ... found_no_account() ... >>> # Характерный для Python способ: >>> if account_number := get_account("123-45-6789"): ... withdraw_money(account_number) ... else: ... found_no_account() 

Assignment Expression

As shown above, when we do not use the assignment expression, we first have to get the account number, and then use it for the withdrawal operation (approx. translation: withdraw_money function), which creates some code duplication. In contrast, we can get rid of a single line of code with an assignment expression that calls the function and assigns the value returned by it to a new variable, which is evaluated at the same time.

Some would argue that saving one line of code is not so important, but this makes our intention more explicit - the variable account_number is relevant only in the scope of the conditional statement. If you have experience programming in Swift, then using an assignment expression in a conditional statement is very similar to the optional binding technique (approx. Transl.: or optional binding), as shown below. In fact, the temporary variable accountNumber, if it contains a value, is used only in the scope that follows. Thus, you should familiarize yourself with the assignment expression, and after a while you will find such code more readable.

if let accountNumber=getAccount("123-45-6789") { withdrawMoney(accountNumber) } else { foundNoAccount() } 

Optional Binding

6. Iteration using the enumerate function

In almost every project, we will inevitably have to force our program to repeat certain operations for all elements in the list, tuple, or some other container data type. These repetitive operations can be accomplished using the for loop. As a rule, we can use its basic form: for element in iterable_object. However, with this iteration, if we need to calculate the current iteration of the loop, it is better to use the enumerate function, which can create a counter for us. Moreover, we can set the initial value of the counter.

>>> # Создаем список студентов для итерирования >>> students=['John', 'Jack', 'Jennifer', 'June'] >>> >>> # Вместо этого: >>> for i in range(len(students)): ... student=students[i] ... print(f"# {i+1}: {student}") ... # 1: John # 2: Jack # 3: Jennifer # 4: June >>> >>> # Характерный для Python способ: >>> for i, student in enumerate(students, 1): ... print(f"# {i}: {student}") ... # 1: John # 2: Jack # 3: Jennifer # 4: June 

Iterate with enumerate

7. Link iterables using zip/zip_longest

Suppose we have two iterable objects, and we want to combine these two objects for each corresponding pair of elements.Usually, you can use the index inversion method, extracting elements from each iterable object in such a way as to be able to combine them to form a dictionary. However, Python has a built-in zip function that does exactly what we want to achieve. In essence, the zip function takes any number of iterable objects and creates an iterable object of the same length as the shortest iterable object. Let's look at an example like this:

>>> # Создаем два списка объектов для использования функции zip >>> students=['John', 'Mike', 'Sam', 'David', 'Danny'] >>> grades=[95, 90, 98, 97] >>> >>> # Вместо этого: >>> grade_tracking0={} >>> for i in range(min(len(students), len(grades))): ... grade_tracking0[students[i]]=grades[i] ... >>> print(grade_tracking0) {'John': 95, 'Mike': 90, 'Sam': 98, 'David': 97} >>> >>> # Предпочтительный способ: >>> grade_tracking1=dict(zip(students, grades)) >>> print(grade_tracking1) {'John': 95, 'Mike': 90, 'Sam': 98, 'David': 97} >>> >>> from itertools import zip_longest >>> grade_tracking2=dict(zip_longest(students, grades)) >>> print(grade_tracking2) {'John': 95, 'Mike': 90, 'Sam': 98, 'David': 97, 'Danny': None} 

Using zip with iterables

The zip function creates an iterator - an object of the zip class, each element of which is a tuple consisting of elements of iterable objects passed to the function. It is also worth noting that, by default, the zip function will stop when elements in an iterable object run out. In contrast, the zip_longest function will focus on the longest iterable object.

The above Python-specific approach takes advantage of the dict constructor, which is the ability to use an iterable object to create an object of class dict. In addition to the use case presented above, an object of the zip class can be used directly, as shown below.

>>> for student, grade in zip(students, grades): ... print(f"{student}: {grade}") ... John: 95 Mike: 90 Sam: 98 David: 97 

Iterating Using the zip Function

8. Concatenation (approx. Transl.: coupling) of iterable objects

In the example above, we used the zip function to merge iterable objects by elements. What if there is a need to concatenate iterable objects? Suppose we need to walk through two iterable objects of the same category to perform the same operation. We can get this opportunity using the chain function. Let's look at an example of its use:

>>> # Определяем данные и вспомогательную функцию >>> from itertools import chain >>> group0=['Jack', 'John', 'David'] >>> group1=['Ann', 'Bill', 'Cathy'] >>> >>> def present_project(student): ... pass ... >>> # Вместо этого: >>> for group in [group0, group1]: ... for student in group: ... present_project(student) ... >>> for student in group0 + group1: ... present_project(student) ... >>> # Характерный для Python способ: >>> for student in chain(group0, group1): ... present_project(student) 

Concatenation of iterable objects

As shown above, an approach uncharacteristic for Python implies the need to create additional lists - not the most memory-efficient way. In contrast, the chain function creates an iterable object from those iterable that have been previously defined. Moreover, the chain function is flexible and can accept iterable objects of any type: dictionaries, sets, lists, objects of the zip and map classes (by using the map function), as well as many other types of iterable Python objects.

9. Ternary expression

What if we need to assign something to a variable so that different values ​​are assigned based on the condition? In this case, we can use the conditional operator to calculate the condition and determine which value should be used for the assignment. This usually involves a few lines of code. However, to cope with this work, we can use a ternary expression that takes up just one line of code and has the following basic syntax: var=value_in_case_true if condition else value_in_case_false. Let's look at relevant examples.

>>> # Определяем вспомогательную функцию >>> from random import randint >>> def got_even_number(): ... return randint(1, 10) % 2 == 0 ... >>> # Вместо этого: >>> if got_even_number(): ... state="Even" ... else: ... state="Odd" ... >>> state="Odd" >>> if got_even_number(): ... state="Even" ... >>> # Характерный для Python способ: >>> state="Even" if got_even_number() else "Odd" 

Ternary expression

10. Using Generators

The concept of generators may not be familiar to beginners in Python, as they are not widely used in many other programming languages. This is a clever trick that allows you to work with a data stream without having to create a stream first. Instead, the generator provides the following value when asked about it, which is very efficient in terms of memory usage.

Consider the following elementary example. Suppose we need to process tons of information in a file. Theoretically, we can read the entire file into a list and process each row of data in the list. However, it is possible that your computer will run out of memory if the file is incredibly large. Instead, there is a solution that is better and more preferable - to create a generator from a file that provides only one row of data for each request.

>>> # Определяем вспомогательную функцию >>> def process_data(row_data): ... pass ... >>> # Вместо этого: >>> with open('large_file') as file: ..."\n") ... for row_data in read_data: ... process_data(row_data) ... >>> # Характерный для Python способ: >>> with open('large_file') as file: ... for row_data in file: ... process_data(row_data) 



There are many more preferable, natural ways for Python to do all kinds of things in Python, and the topics covered in this article are just an incomplete list of tricks that I found useful in my day-to-day projects. I hope they help you write Python code too. The final tip is that you should purposefully refactor your code using these and other Python-specific tricks (approx. Transl.: idioms), which are usually even more efficient in terms of performance. By consistently refactoring your code, you will gradually improve your Python programming skills.

Thanks for reading this material, and good programming.