Performance Optimization: Best Practices when writing code in Python

Author

Davide Vitiello, Mirai Solutions GmbH

Published

March 11, 2025

Understanding Python’s Execution Model

Python is an interpreted language, which means it executes code line by line. This model can introduce overhead, making it essential to optimize critical sections of your code. Understanding how Python manages memory and executes code can help developers make better decisions for performance optimization.

Profiling Python Applications

Before optimizing, it’s important to identify the bottlenecks. Profiling tools like cProfile and line_profiler can help understand where an application spends the most time.

import cProfile

def my_function():
    # Function to profile
    pass

cProfile.run('my_function()')

cProfile.run will provide a detailed report of execution times, helping with targeting specific areas for improvement.

Efficient Use of Data Structures

Choosing the right data structure can significantly impact performance.
Python’s built-in types like lists, dictionaries, and sets are optimized for general cases, but the final choice should depend on the use case:

  • lists for ordered collections of items.
  • dictionaries for a fast lookup by key.
  • collections.defaultdict or collections.Counter for other specialized purposes.

collections.defaultdict

The defaultdict is similar to the standard dictionary (dict), but it provides a default value for missing keys. The default value is provided at the time of its creation.
This feature can simplify code that adds items to dictionaries and can significantly reduce the need for checks before assignments or updates.

For example, when grouping items or counting occurrences, using a defaultdict can eliminate the need to check if a key already exists.

from collections import defaultdict

words = ["apple", "banana", "apple", "pear", "banana", "orange"]
word_counts = defaultdict(int)  # default value of 0 for int

for word in words:
    word_counts[word] += 1

word_counts
defaultdict(int, {'apple': 2, 'banana': 2, 'pear': 1, 'orange': 1})

collections.Counter

The Counter is a subclass of dictionary designed for counting hashable objects. It’s a convenient and efficient way to count occurrences of items and has methods for common patterns, such as finding the most common items.

Using a Counter is straightforward and eliminates the need for manual counting logic.

from collections import Counter

words = ["apple", "banana", "apple", "pear", "banana", "orange"]
word_counts = Counter(words)

word_counts
Counter({'apple': 2, 'banana': 2, 'pear': 1, 'orange': 1})
word_counts.most_common(2)
[('apple', 2), ('banana', 2)]

Minimizing Global Variable Access

Access to global variables is slower than local ones due to the way Python’s scope resolution works. Minimizing the use of global variables or caching them locally when they are used frequently within a function is advisable.

Utilizing List Comprehensions and Generator Expressions

List comprehensions and generator expressions are not only more concise but often faster than traditional for and while loops.

List comprehension:

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

Generator expression:

sum_of_squares = sum(x**2 for x in range(10))

Leveraging Built-in Functions and Libraries

Python’s standard library is highly optimized. Whenever possible, one should prefer Python’s built-in functions and libraries which are often implemented in C, offering better performance.

Avoiding Unnecessary Abstractions

While abstractions can make code more readable and maintainable, they can also introduce performance overhead. Evaluating the necessity of each abstraction layer and simplifying where possible are advisable.

Multi-threading and Multi-processing

Python’s GIL (Global Interpreter Lock) limits the execution of multiple threads in a single process, which can be a bottleneck for CPU-bound tasks. For parallel execution, consider using the multiprocessing module to take advantage of multiple cores.

Back to top