Blogs
Top 10 Tips and Tricks in Python to Write Better Code
- November 30, 2022
- Category: Programming Python Technology

Python is popular amongst developers and its simple syntax makes it a great first programming language to learn. Unlike programming languages like C and C++, Python has built-in functions and features that let you do certain operations with a concise and intuitive syntax. This article presents useful tips that’ll help you write better Python code.
Let’s dive right in!
1. Use List Comprehensions Instead of Verbose for Loops
To create a Python list from an existing iterable, you can use list comprehensions. Consider the list nums. You’d like to create a new list, thrice_nums, that contains each element of the nums list multiplied by a factor of 3.
You can use a for
loop to do this:
nums = [3,4,7,9]
thrice_nums = []
for num in nums:
thrice_nums.append(num*3)
print(thrice_nums)
# [9, 12, 21, 27]
The above code does the following:
– loops through the nums
list,
– accesses each num
and multiplies it by 3 (num*3
), and
– appends it to the thrice_nums
list.
But list comprehensions provide a more concise alternative to for
loops. Here’s the syntax:
From the explicit for loop, we can identify the following:
- Output:
num*3
- For item in this-list:
for num in nums
Putting it all together, this is the list comprehension expression to get the thrice_nums
list:
thrice_nums = [num*3 for num in nums]
print(thrice_nums)
# [9, 12, 21, 27]
You can also use list comprehension to filter through existing lists based on some condition. Here is the general syntax to do so:
As an example, let’s get all the odd numbers from the nums list. For a number to be odd, it should not be evenly divisible by zero; so we specify that as the condition (num%2!=0).
nums = [1,5,7,3,4,8,2]
odd = [num for num in nums if num%2!=0]
print(odd)
# [1, 5, 7, 3]
2. Use enumerate() to Access Both the Indices and Elements in an Iterable
In Python, lists, tuples, and strings are some of the commonly used iterables. You can loop through them and access the individual elements. If you’re familiar with looping in Python, you’d have likely used for loops to access items in a list.
Here, to_learn is a Python list of strings. You can access individual items using a for loop:
to_learn = ['sql', 'bash scripting', 'scala', 'docker']
for elt in to_learn:
print(elt)
# Output
sql
bash scripting
scala
docker
But how do you access the index of each item as well?
To do this, you can use the range() function to get the list of valid indices, and then access both the index and the item. Python follows zero indexing. So for a list of length (returned by the len() function), the first element is at index 0, and the last element is at index len(list-obj) -1.
for i in range(len(to_learn)):
print(i,to_learn[i])
# Output
0 sql
1 bash scripting
2 scala
3 docker
Using the enumerate()
function with the syntax enumerate(iterable)
, you can access both the index and the element simultaneously, as shown:
for index, elt in enumerate(to_learn):
print(index, elt)
# Output
0 sql
1 bash scripting
2 scala
3 docker
The index starts at zero by default; however, when specifying the arguments to the enumerate()
function, you can optionally specify an index to start at (for example, 1).
for index, elt in enumerate(to_learn,1):
print(index,elt)
# Output
1 sql
2 bash scripting
3 scala
4 docker
3. Improve Readability of Immutable Collections Using NamedTuples
In Python, tuples are immutable collections, so you cannot modify them in place. You can use them to store values that should not be modified in the rest of the program. However, you cannot define what each value in the tuple stands for.
Consider the following tuple of three numbers, that stand for RGB values of a color shade.
color = (127,234,100) # It’ll help to have a description of what these numbers are!
Enter NamedTuple…
With namedtuple, you have both readability and immutability.
The namedtuple
is available in the collections
module that is built into the Python standard library so you can import it into your working environment:
from collections import namedtuple
You can use the syntax namedtuple(Name,[Attribute-list])
to define a namedtuple.
color = namedtuple("Color",['r','g','b'])
This namedtuple, Color, serves as a template from which you can create many color tuples, each with its own r, g, and b values.
red = Color(255,0,0)
white = Color(255,255,255)
You can use the dot notation with the syntax namedtuple.attribute_name
to access the individual attributes.
Note: You can code along in a Python REPL.
>>> red.r
255
>>> red.g
0
>>> red.b
0
>>> white.r
255
>>> white.g
255
>>> white.b
255
Here’s another example where we define a Book namedtuple with the attributes title, author, and rating.
Book = namedtuple('Book',['title', 'author', 'rating'])
book1 = Book('Grit', 'Angela Duckworth',5)
>>> book1.title
'Grit'
>>> book1.author
'Angela Duckworth'
>>> book1.rating
5
4. Use Context Managers to Manage Resources Efficiently
In Python, context managers handle the setting up and cleaning up of resources.
Let’s take file handling operation as an example. In the following code snippet, the open()
function returns a file handler object. We then write a line into the file. But we’ll have to close the file explicitly after the write operation.
f = open('newfile.txt','w')
f.write('Hello, there!')
If you check the closed attribute of the file object f, it’s False as the file is still open.
print(f.closed)
# False
You should explicitly close the file.
f.close()
print(f.closed)
# False
Using the with
keyword with the open()
function ensures that a context manager controls the operations.
with open('another_file.txt', 'w')as f:
f.write('Hello!')
We did not close the file, but the closed attribute is True as the context manager handles this step for us.
print(f.closed)
# True
In addition, you can also define custom context managers for requesting locks, opening and closing database connections, and more.
5. Chain Multiple Conditions Using any() and all()
When you’d like to chain multiple conditions using logical OR and AND, you can use the any() and all() functions in Python.
Use any() for Chained OR Conditions
Consider the following example where the discount is based on the price, number of visits to the store, and previous discounts obtained. We have three conditions chained by `or` to check if the customer qualifies for a discount.
price = 1000
num_visits = 10
previous_discount = 10
if (price >= 1000) or (num_visits>=10) or (previous_discount <=10):
final_price = 0.8*price # price - 0.2*price = 0.8*price
print(final_price)
# 800.0
You can make this concise using the any() function. In Python, any(iterable)
returns True
if at least one element in the iterable evaluates to True
– equivalent to the logical OR operation.
c1 = price >=1000
c2 = num_visits>=10
c3 = previous_discount <=10
Collecting the above conditions, c1
, c2
, and c3
into an iterable, we have:
if any([c1,c2,c3]):
final_price = 0.8*price
print(final_price)
# 800.0
Use all() for Chained AND Conditions
Let’s modify the example. This time, a customer is eligible for a discount only if all the conditions are True
. We chain the conditions using the logical AND operator.
price = 1000
num_visits = 10
previous_discount = 12
if (price>=1000) and (num_visits>=10) and (previous_discount<=10):
final_price = 0.8*price
else:
final_price = price
In Python, all(iterable)
returns True
only if all items in the iterable evaluate to True
. So we can modify the if-else block using all()
as shown:
c1 = price >=1000
c2 = num_visits>=10
c3 = previous_discount <=10
if all([c1,c2,c3]):
final_price = 0.8*price
else:
final_price = price
print(final_price)
1000
6. Sort Python Iterables Using sorted() and Lambdas
Sorting is a common operation when working with Python iterables. To obtain a sorted copy of the iterables, you can use the sorted() function and also customize the sorting using the optional key and reverse parameters.
How to Sort Python Lists
Consider the following list, skills:
skills = ['statistics', 'sql', 'data cleaning', 'reporting', 'model building']
Calling the sorted()
with the list as the argument returns a sorted copy of the list, where the strings are sorted in alphabetical order.
sorted_skills = sorted(skills)
print(sorted_skills)
# ['data cleaning', 'model building', 'reporting', 'sql', 'statistics']
Now if you set the optional key parameter to the built-in len function, the sorting will be done based on the length of the string. Notice how the shortest string ‘sql’ and the longest string ‘model building’ are the first and the last elements in the sorted list, respectively.
sorted_skills = sorted(skills,key=len)
# ['sql', 'reporting', 'statistics', 'data cleaning', 'model building']
Setting reverse = True
reverses the sorting order. The longest string appears first.
sorted_skills = sorted(skills,key=len,reverse=True)
# ['model building', 'data cleaning', 'statistics', 'reporting', 'sql']
How to Sort Python Dictionaries
You may also want to sort the items in a Python dictionary. You can call the .items() method to get the keys and values in a list of tuples.
Here’s an example:
data_skills = {'sql':5,'pandas':4,'model building':3,'data visualization':4.5}
print(data_skills.items())
# dict_items([('sql', 5), ('pandas', 4), ('model building', 3), ('data visualization', 4.5)])
You can then sort this list of tuples and cast the result into a dictionary using the dict() function. Note that the default sorting is in terms of the alphabetical order of the keys.
sorted_dict = dict(sorted(data_skills.items()))
print(sorted_dict)
# {'data visualization': 4.5, 'model building': 3, 'pandas': 4, 'sql': 5}
Recall that each tuple is of the form (key, value). To sort by value, you’ll have to pull the value which is at index 1 in the tuple. To do this, you can use a lambda expression: lambda args: expression
is the general syntax. Here, for a tuple x
, we need x[1]
.
sorted_dict = dict(sorted(data_skills.items(),key=lambda i:i[1]))
print(sorted_dict)
# {'model building': 3, 'pandas': 4, 'data visualization': 4.5, 'sql': 5}
sorted_dict = dict(sorted(data_skills.items(),key=lambda i:i[1],reverse=True))
print(sorted_dict)
# {'sql': 5, 'data visualization': 4.5, 'pandas': 4, 'model building': 3}
7. Use zip() to Iterate Through Multiple Iterables
When you want to loop through multiple lists (or any iterable) at the same time, you should use the zip() function.
Here, prices and discounts are two separate lists. You’d like to create a new list, final_prices, that applies the discount on each element in the prices list and returns the final list.
prices = [100,345,231,99,85]
discounts = [2,10,3.5,12,9]
You can always use a for loop in conjunction with the range() function:
for i in range(len(prices)):
discounted_price = prices[i] - 0.01*discounts[i]*prices[i]
final_prices.append(discounted_price)
print(final_prices)
# [98.0, 310.5, 222.915, 87.12, 77.35]
Using the zip()
function is more intuitive and makes your code easy to read.
zip(list1,list2,...,list_n)
to zips together n lists. Under the hood, zip()
function returns an iterator of the tuples. In this example, the first element of the iterator will contain the first element in the prices and discounts list (prices[0], discounts[0]), the second element will be (prices[1], discounts[1]), and so on.
So you can unpack these two elements into two variables, namely, price and discount. We’ve used list comprehension as it is more concise.
final_prices = [price - 0.01*discount*price for price,discount in zip(prices,discounts)]
print(final_prices)
# [98.0, 310.5, 222.915, 87.12, 77.35]
8. Use *args and **kwargs Effectively
When you want a function to take in a variable number of positional and keyword arguments, you can use *args and **kwargs, respectively.
The following function, sum_nums
, returns the sum of all numbers that are passed in as the arguments in the function call:
def sum_nums(*args):
sum = 0
for num in args:
sum += num
return sum
sum_nums(1,2,3,4,5)
# 15
sum_nums(3,8,9)
# 20
Using *args lets you handle a variable number of arguments—without having to define separate functions for a specific number of arguments.
Similarly, you can pass in a variable number of keyword arguments using **kwargs.
def func(*args,**kwargs):
sum = 0
for num in args:
sum += num
for name,value in kwargs.items():
sum += value
return sum
We pass in two positional arguments, 2,3 and three keyword arguments, x,y, and z.
func(2,3,x=4,y=7,z=3)
# 19
Note: It is recommended to use *args and **kwargs, but you can use any other names for these parameters.
9. Use Generator Expressions for Memory Efficiency
In Python, you should use generators when you need to work with individual elements of a long sequence.
Consider the following list comprehension expression:
list_1= [i*2 for i in range(10)]
print(list_1)
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
The list is created ahead of time and stored in memory. This can be inefficient if the list is too long. Here’s where generators can help.
To define a generator expression, you can replace the square brackets with parentheses. Upon printing out, you can see that gen_1 is a generator object.
gen_1 = (i*2 for i in range(10))
print(gen_1)
# <generator object <genexpr> at 0x00DC64F8>
Now, you can call the next() function on the generator object to get the elements in the sequence.
next(gen_1)
# 0
Notice that every subsequent call to the next() function returns the next element in the sequence.
next(gen_1)
# 2
next(gen_1)
# 4
You can also loop through the generator object just the way you’d loop through a range() object. As we’ve already accessed the elements, the next element is 8.
for i in gen_1:
print(i)
# Output
6
8
10
12
14
16
18
As generator expression yields the subsequent next elements on demand—instead of storing the entire iterable—it helps in memory efficiency.
10. Time Python Functions Using timeit
When you’re defining functions, it’s often helpful to time them and measure how long they take to run. This will help you optimize code when working on a large project with multiple modules.
As an example, let’s define two functions, func1() and func2(), that return a list of the squares of all numbers in the range [0,1000). The range() function excludes the endpoint by default, so when you loop through range(n), you’ll get 0, 1, 2, up to n-1. Here, n = 1000, so we’ll get the numbers from 0 to 999.
def func1():
new_list = []
for i in range(1000):
new_list.append(i*i)
return new_list
def func2():
new_list = [i*i for i in range(1000)]
return new_list
-
func1()
uses an explicit for loop to loop through the numbers 0, 1, 2, …, 999. It then appends the square of the number to the new list, one number at a time. -
func2()
uses list comprehension to return the list of squares.
How to Time Python Functions Using timeit.timeit()
Python has a built-in timeit
module that you can use to measure the speed of execution of functions.
To time Python functions, you can use the timeit()
function from the timeit
module with the syntax: timeit.timeit(callable, number=...)
. You can pass in any Python callable such as a function and number
denotes the number of times you’d like to run the function. In general, you may want to find the execution time for multiple runs.
The execution times for func1()
and func2()
are captured in the variables time_func1
and time_func2
, respectively.
import timeit
time_func1 = timeit.timeit(func1,number=1000)
time_func2 = timeit.timeit(func2,number=1000)
We see that func2()
using list comprehension runs marginally faster than the function using for loop.
print(time_func1)
# 0.4373901000000018
print(time_func2)
# 0.2894799999999975
We can measure the ratio of the execution times and round the result to three decimal places for better readability.
print(time_func2/time_func1)
# 0.6618348243364364
print(f"func1 is {round(time_func2/time_func1,3)} times faster than func1.")
# func1 is 0.662 times faster than func1.
Conclusion
I hope you find this tutorial on Python tips helpful! If you’re looking to learn Python, from the basics to more advanced concepts, check out this Python programming for data science path—with resources, hands-on coding exercises, and more. Happy learning!