6 Intermediate Python Tips For Better Code

Codecast is a reader supported publication. Feel free to subscribe to receive regular bite-sized articles on programming topics:
Generator expressions
Generators are the memory-efficient way of dealing with large iterables, data structures, and files in Python. They implement lazy loading as a key feature and are thus employed for most memory-intensive tasks.
Consider a simple example: reading a file object line by line and performing some operation. The naive way to do this in case of both big and small files is by loading the whole thing into memory. Like this:
def read_fully():
with open('big_file.txt', 'r') as file_object:
for line in file_object.readlines():
pass_to_some_function(line)
# do something else
Obviously, this is a bad idea for any medium sized to large sized files. What we can do instead is make use of generators. Simply replace this with the generator equivalent function with something like:
def read_lines(file_object):
while True:
data = file_object.readlines()
if not data:
break
yield data
Now, instead of passing the values one by one to another function, simply call this generator as an iterator in that function:
def pass_to_some_function():
with open('big_file.txt', 'r') as file_object:
for line in read_lines(file_object):
# do something with the file chunk
And that is it! It’s handsomely convenient that this will not result in any annoying memory errors and unexpected slowdowns during execution.
Iterable Filters
When we’re only interested in traversing over a part of the list subject to a given condition, we can use filters directly in iterable loops.
An ordinary iterable in a for loop will look like this:
for person_name in list_of_people:
if person_name.startswith("Th"):
print(person_name)
Now, instead of that if condition, we can use the filter function in the loop itself. Something like this:
for person_name in filter(lambda name: name.startswith("Th"), list_of_people):
print(person_name)
Perfect, innit? We got rid of the if-condition quite handily.
Sorting Dictionaries
Sorting arrays or lists is quite easily done with the following:
my_list.sort()
Now, how do we sort dictionaries? If we have something like this:
my_dict = {'A': 2, 'B': 56, 'C': 0, 'D': 10, 'E': 8}
I have found myself looking through stackoverflow multiple times in the past for this before I realized I can remember this through a simple trick.
If we want the dictionary to be sorted by values, we can do this:
my_sorted_dict = sorted(my_dict.items(), key = lambda item: item[1])
What we’re doing here is utilizing the “sorted” function in Python. Remember that dict.items()
function returns a tuple of key and value of every item in our dictionary?
The “sorted” function takes in an additional “key” parameter that can be used for sorting with certain conditions, in this case, we simply specify a lambda function and assign the value “item[1]” ( the value of each key in the dictionary ) as equal to the key (the value of the “key” parameter of the sorted function).
And we get our desired result!
Type Hints
Reading through the codebase of an unfamiliar project is a daunting task in and of itself, and that is without wondering about the arguments of every single function we come across. Hence, type hints.
We use type hints for better readability for code such as:
def my_func(foo, bar):
# do something
Now, imagine if we specified something like this:
def my_func(foo: str, bar: int):
# do something
We have an additional package called the typing package which provides types such as Any, Union, Generic etc.
Tuple Unpacking
Imagine that you have a tuple of several items with three variable to assign them into. What you’d normally do is something like this:
for index, item in enumerate(tuple):
# assign variables one by one using index
A better way is to use tuple unpacking using the “*” operator.
Simply do it like this:
my_tuple = (1, 2, 3, 4, 5, 6)
# assign to start, middle, and end variables
start, *middle, end = my_tuple
The values assigned to the three variables will be: start has the first element 1, end has the last element 6, and the rest of the elements went to the middle variable as a list.
If we print them together, we get:
print(start, middle, end)
# 1 [2, 3, 4, 5] 6
Very convenient, isn’t it?
Lambda Functions
Small operations for which we don’t want to have to define a standard function can be handled with the help of lambda functions. They are the anonymous functions that can perform a simple operation that we want to out-source to an external function as just a one-liner.
Let’s look at an example: how to handle a simple out-of-bounds error when traversing a matrix ( a list of lists ):
in_bounds = lambda i, j: True if i >= 0 and i < rows and j >= 0 and j < cols else False
This function takes in the number of rows and columns and checks if they’re more than zero and less than the length. Quite a simple check when traversing a matrix which now, due to our lambda function, does not need to be repeatedly defined everywhere.
We can simply call and use this function (check) anywhere like this:
if in_bounds(row_id, column_id):
# do something
A few parting words
I hope this was a useful read on some of the little Pythonic details that you can use to make your code feel more elegant and enhance readability for anyone who reads it without too much domain knowledge or context.
Thank you for reading!
If you enjoyed this issue, feel free to share it with a friend!