Efficient Python Coding Tips for Developers

You write line after line, debug, rewrite, and then optimize and while Python makes life easier with its readable syntax and rich libraries — are you using it efficiently? Here are some tips and tricks to help you code smarter and faster in Python. Even if you’re an experienced developer, a few tweaks can make a big difference in performance and readability.

Make Use of List Comprehensions

List comprehensions are a cleaner and faster way to create lists. Instead of using loops, you can generate a list in one line. This improves both readability and speed.

Long Way: Appending to Lists with Loops

Imagine you want a list of squares for numbers from 0 to 9. Here’s how you might do it traditionally:

squares = []
for x in range(10):
    squares.append(x**2)
print(squares)

This method works, but it’s lengthy. You have to initialize an empty list, loop through each number, and append the square of each number to the list.

Condensed Way: Using List Comprehensions

Using list comprehensions, you can achieve the same result in a single line:

squares = [x**2 for x in range(10)]
print(squares)

Here, you generate the list with a simple, clear expression. It’s not only shorter but also improves readability. List comprehensions are perfect for creating new lists by performing an operation on each element of an existing list or range.

Dictionary Comprehensions: Simplifying Dictionary Construction

Like lists, dictionaries can also be constructed in a more compact way. Suppose you want a dictionary where keys are numbers and values are their squares.

Long Way: Using Loops for Dictionary Construction

squares_dict = {}
for x in range(10):
    squares_dict[x] = x**2
print(squares_dict)

In this version, you manually add each key-value pair to the dictionary. It’s clear but involves several lines of code.

Condensed Way: Using Dictionary Comprehensions

squares_dict = {x: x**2 for x in range(10)}
print(squares_dict)

This single line achieves the same outcome. The dictionary comprehension directly maps each key to its value, simplifying the construction process.

Unpacking: Deconstructing Data Structures Easily

Unpacking lets you break down data structures like lists and tuples into individual elements. It’s useful when you need to assign multiple variables in one step.

Long Way: Assigning Variables Separately

grades = [85, 90, 78, 92, 88]
first_grade = grades[0]
second_grade = grades[1]
third_grade = grades[2]
print(first_grade, second_grade, third_grade)

Each variable gets assigned in a separate line. This approach works but becomes unwieldy with more elements.

Condensed Way: Unpacking in One Step

grades = [85, 90, 78, 92, 88]
first_grade, second_grade, third_grade, *remaining_grades = grades
print(first_grade, second_grade, third_grade)

With unpacking, you can assign multiple variables at once. The *remaining_grades captures any leftover values. This keeps your code clean and flexible.

zip() for Parallel Iteration: Synchronized Loops

Often, you need to iterate over multiple lists in parallel. Instead of using indices, you can use zip() to streamline this process.

Long Way: Using Indices for Parallel Iteration

names = ['Alice', 'Bob', 'Charlie']
scores = [85, 90, 95]
for i in range(len(names)):
    print(f"{names[i]} scored {scores[i]}")

This approach works but involves managing indices, which can lead to errors.

Condensed Way: Using zip()

names = ['Alice', 'Bob', 'Charlie']
scores = [85, 90, 95]
for name, score in zip(names, scores):
    print(f"{name} scored {score}")

With zip(), you iterate over both lists at once, keeping your code neat and intuitive.

Avoid Mutable Default Arguments: Common Pitfall

Default arguments in Python can be tricky, especially when they involve mutable objects like lists or dictionaries.

Long Way: Using Mutable Default Arguments (Error-Prone)

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item('apple'))
print(add_item('banana'))

This code seems fine at first glance. But it reuses the same list for every function call, leading to unexpected results.

Correct Way: Using None as a Default

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item('apple'))
print(add_item('banana'))

This way, each function call gets a new list, preventing unwanted side effects.

collections Module: Powerful Data Structures

Python’s collections module offers enhanced alternatives to built-in data types. Structures like defaultdictCounter, and deque can simplify complex tasks.

Long Way: Manual Counting with Dictionaries

words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
word_count = {}
for word in words:
    if word in word_count:
        word_count[word] += 1
    else:
        word_count[word] = 1
print(word_count)

This code counts word frequency using a standard dictionary. It works but involves repetitive logic.

Condensed Way: Using Counter

from collections import Counter
words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
word_count = Counter(words)
print(word_count)

The Counter object handles all the counting, making the code much more straightforward.

Generators: Efficient Memory Usage

Generators produce values one at a time, which can save memory when dealing with large data sets.

Long Way: Creating Lists “[]”

numbers = [x**2 for x in range(1000000)]
# Process numbers here

This creates a large list in memory, which can be inefficient.

Condensed Way: Using Generators “()”

numbers = (x**2 for x in range(1000000))
# Process numbers here

The code example above looks similar at a glance but function quite differently in terms of memory usage.

This version generates values as needed, keeping memory usage low.

set Operations: Simplify Membership Tests

Sets provide a fast way to check for membership, uniqueness, and common elements between collections.

Long Way: Using Lists for Membership Tests

list1 = [1, 2, 3, 4, 5]
list2 = [4, 5, 6, 7, 8]
common_elements = []
for item in list1:
    if item in list2:
        common_elements.append(item)
print(common_elements)

This approach uses nested loops and manual checks, which can be slow.

Condensed Way: Using Sets

set1 = {1, 2, 3, 4, 5}
set2 = {4, 5, 6, 7, 8}
common_elements = set1 & set2
print(common_elements)

The set intersection & operator quickly finds common elements, making your code concise and efficient.

Merging Dictionaries: Cleaner Merging

Combining multiple dictionaries is a common task. Python provides elegant ways to do this.

Long Way: Using update() Method

dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
merged_dict = dict1.copy()
merged_dict.update(dict2)
print(merged_dict)

This approach involves making a copy of one dictionary and then updating it with the other. It’s functional but not very elegant.

Condensed Way: Dictionary Merge Operator

In Python 3.9 and later, you can use the | operator:

dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
merged_dict = dict1 | dict2
print(merged_dict)

This syntax is cleaner and directly conveys your intention to merge dictionaries.

map() and filter():Functional Programming Techniques

Python supports functional programming styles with functions like map() and filter().

Long Way: Using Loops for Mapping

numbers = [1, 2, 3, 4, 5]
squared = []
for n in numbers:
    squared.append(n**2)
print(squared)

This loop squares each number and appends it to a list, which works but requires multiple steps.

Condensed Way: Using map()

numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(squared)

The map() function applies a transformation to each element, producing the desired list in one concise statement.

Use sorted() with Custom Keys: Flexible Sorting

Sorting data often requires custom rules. The sorted() function allows for flexible, clear sorting based on custom criteria.

Long Way: Sorting with sort() and Loops

Imagine you have a list of dictionaries representing students with their names and grades. You want to sort this list by grade.

students = [
    {'name': 'John', 'grade': 90},
    {'name': 'Jane', 'grade': 85},
    {'name': 'Dave', 'grade': 92}
]

def get_grade(student):
    return student['grade']

students.sort(key=get_grade)
print(students)

In this approach, you define a function to extract the grade from each student dictionary. Then, you pass this function to the sort() method. While this works, it requires defining an additional function, which can clutter your code.

Condensed Way: Using sorted() with a Lambda Function

The same result can be achieved in a more compact way using the sorted() function with a lambda function:

students = [
    {'name': 'John', 'grade': 90},
    {'name': 'Jane', 'grade': 85},
    {'name': 'Dave', 'grade': 92}
]

sorted_students = sorted(students, key=lambda student: student['grade'])
print(sorted_students)

The lambda function here directly extracts the grade from each student, making the code shorter and clearer. The sorted() function returns a new sorted list, keeping the original list unchanged. This approach is flexible and expressive, allowing you to sort based on various criteria without defining separate functions.

Using defaultdict: Simplify Dictionary Operations

When dealing with dictionaries, you often need to initialize default values. The defaultdict from the collections module can simplify this process.

Long Way: Initializing Default Values Manually

Suppose you have a list of words and want to count their occurrences in a dictionary:

word_list = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
word_count = {}

for word in word_list:
    if word in word_count:
        word_count[word] += 1
    else:
        word_count[word] = 1

print(word_count)

In this example, you check if a word is already in the dictionary. If not, you add it with a default value of 1. This approach works but involves repetitive code.

Condensed Way: Using defaultdict

With defaultdict, you can simplify the same operation:

from collections import defaultdict

word_list = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
word_count = defaultdict(int)

for word in word_list:
    word_count[word] += 1

print(word_count)

The defaultdict automatically assigns a default value (in this case, 0 for integers) when a new key is accessed. This eliminates the need for checking and initializing keys, reducing boilerplate code.

Using itertools: Streamline Complex Iterations

The itertools module offers powerful tools for complex iterations and combinations, making your code more efficient and expressive.

Long Way: Manual Combinations with Nested Loops

Suppose you want to find all possible pairs of elements from two lists:

list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
pairs = []

for x in list1:
    for y in list2:
        pairs.append((x, y))

print(pairs)

This approach uses nested loops to generate all possible pairs. It works, but the nested structure can be cumbersome and less readable.

Condensed Way: Using itertools.product

You can achieve the same result with the product function from itertools:

from itertools import product

list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
pairs = list(product(list1, list2))

print(pairs)

The product() function generates the Cartesian product of the input iterables, providing a clear and concise way to create all pairs. It simplifies the code and eliminates the need for nested loops.

f-strings: Simplify String Formatting

Formatting strings often involves combining variables and text. The newer f-string syntax provides a more readable and efficient way to do this compared to older methods.

Long Way: Using .format() Method

name = "Alice"
age = 30
greeting = "Hello, my name is {} and I am {} years old.".format(name, age)
print(greeting)

The .format() method replaces placeholders with variables. It works but can be cumbersome with many variables.

Condensed Way: Using f-strings

name = "Alice"
age = 30
greeting = f"Hello, my name is {name} and I am {age} years old."
print(greeting)

With f-strings, you can directly embed variables in the string, making your code more readable and concise.

Using enumerate(): Cleaner Looping with Indices

Often, you need both the index and value of elements in a list. The enumerate() function provides a clean solution.

Long Way: Manual Index Tracking

items = ['apple', 'banana', 'cherry']
for i in range(len(items)):
    print(f"Item {i}: {items[i]}")

Here, you manually track indices using range() and len(). It works but adds complexity.

Condensed Way: Using enumerate()

items = ['apple', 'banana', 'cherry']
for i, item in enumerate(items):
    print(f"Item {i}: {item}")

With enumerate(), you access both index and value in a single statement, improving readability and reducing code.

Use any() and all(): Simplify Condition Checking

Checking multiple conditions can lead to long and complex code. The any() and all() functions help streamline these checks.

Long Way: Manual Condition Checks

Suppose you want to check if any item in a list is greater than 10:

numbers = [4, 9, 11, 7]
found = False

for number in numbers:
    if number > 10:
        found = True
        break

print(found)

This code manually iterates through the list, setting a flag if the condition is met. It works, but it’s verbose.

Condensed Way: Using any()

numbers = [4, 9, 11, 7]
found = any(number > 10 for number in numbers)
print(found)

The any() function checks if any condition in the generator expression is true, simplifying the code significantly.


Thank you for following along with this tutorial. We hope you found it helpful and informative. If you have any questions, or if you would like to suggest new Python code examples or topics for future tutorials/articles, please feel free to join and comment. Your feedback and suggestions are always welcome!

You can find the same tutorial on Medium.com.

Leave a Reply