Python Code Quality Illustration

Python Code Quality: Best Practices and Tools

by Leodanis Pozo Ramos Mar 24, 2025 8 Comments intermediate best-practices python tools

Producing high-quality Python code involves using appropriate tools and consistently applying best practices. High-quality code is functional, readable, maintainable, efficient, and secure. It adheres to established standards and has excellent documentation.

You can achieve these qualities by following best practices such as descriptive naming, consistent coding style, modular design, and robust error handling. To help you with all this, you can use tools such as linters, formatters, and profilers.

By the end of this tutorial, you’ll understand that:

  • Checking the quality of Python code involves using tools like linters and static type checkers to ensure adherence to coding standards and detect potential errors.
  • Writing quality code in Python requires following best practices, such as clear naming conventions, modular design, and comprehensive testing.
  • Good Python code is characterized by readability, maintainability, efficiency, and adherence to standards like PEP 8.
  • Making Python code look good involves using formatters to ensure consistent styling and readability, aligning with established coding styles.
  • Making Python code readable means using descriptive names for variables, functions, classes, modules, and packages.

Read on to learn more about the strategies, tools, and best practices that will help you write high-quality Python code.

Take the Quiz: Test your knowledge with our interactive “Python Code Quality: Best Practices and Tools” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Python Code Quality: Best Practices and Tools

In this quiz, you'll test your understanding of Python code quality, tools, and best practices. By working through this quiz, you'll revisit the importance of producing high-quality Python code that's functional, readable, maintainable, efficient, and secure.

Defining Code Quality

Of course you want quality code. Who wouldn’t? But what is code quality? It turns out that the term can mean different things to different people.

One way to approach code quality is to look at the two ends of the quality spectrum:

  • Low-quality code: It has the minimal required characteristics to be functional.
  • High-quality code: It has all the necessary characteristics that make it work reliably, efficiently, and effectively, while also being straightforward to maintain.

In the following sections, you’ll learn about these two quality classifications and their defining characteristics in more detail.

Low-Quality Code

Low-quality code typically has only the minimal required characteristics to be functional. It may not be elegant, efficient, or easy to maintain, but at the very least, it meets the following basic criteria:

  • It does what it’s supposed to do. If the code doesn’t meet its requirements, then it isn’t quality code. You build software to perform a task. If it fails to do so, then it can’t be considered quality code.
  • It doesn’t contain critical errors. If the code has issues and errors or causes you problems, then you probably wouldn’t call it quality code. If it’s too low-quality and becomes unusable, then if falls below even basic quality standards and you may stop using it altogether.

While simplistic, these two characteristics are generally accepted as the baseline of functional but low-quality code. Low-quality code may work, but it often lacks readability, maintainability, and efficiency, making it difficult to scale or improve.

High-Quality Code

Now, here’s an extended list of the key characteristics that define high-quality code:

  • Functionality: Works as expected and fulfills its intended purpose.
  • Readability: Is easy for humans to understand.
  • Documentation: Clearly explains its purpose and usage.
  • Standards Compliance: Adheres to conventions and guidelines, such as PEP 8.
  • Reusability: Can be used in different contexts without modification.
  • Maintainability: Allows for modifications and extensions without introducing bugs.
  • Robustness: Handles errors and unexpected inputs effectively.
  • Testability: Can be easily verified for correctness.
  • Efficiency: Optimizes time and resource usage.
  • Scalability: Handles increased data loads or complexity without degradation.
  • Security: Protects against vulnerabilities and malicious inputs.

In short, high-quality code is functional, readable, maintainable, and robust. It follows best practices, including clear naming, consistent coding style, modular design, proper error handling, and adherence to coding standards. It’s also well-documented and easy to test and scale. Finally, high-quality code is efficient and secure, ensuring reliability and safe use.

All the characteristics above allow developers to understand, modify, and extend a Python codebase with minimal effort.

The Importance of Code Quality

To understand why code quality matters, you’ll revisit the characteristics of high-quality code from the previous section and examine their impact:

  • Functional code: Ensures correct behavior and expected outcomes.
  • Readable code: Makes understanding and maintaining code easier.
  • Documented code: Clarifies the correct and recommended way for others to use it.
  • Compliant code: Promotes consistency and allows collaboration.
  • Reusable code: Saves time by allowing code reuse.
  • Maintainable code: Supports updates, improvements, and extensions with ease.
  • Robust code: Minimizes crashes and produces fewer edge-case issues.
  • Testable code: Simplifies verification of correctness through code testing.
  • Efficient code: Runs faster and conserves system resources.
  • Scalable code: Supports growing projects and increasing data loads.
  • Secure code: Provides safeguards against system loopholes and compromised inputs.

The quality of your code matters because it produces code that’s easier to understand, modify, and extend over time. It leads to faster debugging, smoother feature development, reduced costs, and better user satisfaction while ensuring security and scalability.

Exploring Code Quality in Python With Examples

In the following sections, you’ll dive into some short code examples that will make evident the importance of each of the characteristics of high-quality Python code.

Functionality

The most important factor when evaluating the quality of a piece of code is whether it can do what it’s supposed to do. If this factor isn’t achieved, then there’s no room for discussion about the code’s quality.

Consider the following quick example of a function that adds two numbers. You’ll start with a low-quality implementation of the function.

🔴 Low-quality code:

Python
>>> def add_numbers(a, b):
...     return a + b
...

>>> add_numbers(2, 3)
5

Your add_numbers() function seems to work well. However, if you dig deeper into the implementation, you’ll note that if you mix some argument types, then the function will crash:

Python
>>> add_numbers(2, "3")
Traceback (most recent call last):
    ...
TypeError: unsupported operand type(s) for +: 'int' and 'str'

In this call to add_numbers(), you pass an integer and a string. The function tries to add them, but Python comes up with an error because it’s impossible to add numbers and strings. Now, it’s time for a higher-quality implementation.

Higher-quality code:

Python
>>> def add_numbers(a: int | float, b: int | float) -> float:
...     a, b = float(a), float(b)
...     return a + b
...

>>> add_numbers(2, 3)
5.0

>>> add_numbers(2, "3")
5.0

When looking at this new implementation, you’ll quickly realize by inspecting the type annotations that the function should now be called with numeric values of type int or float. When you call it with numbers, it works as expected.

Now, what if you violate the argument types? The highlighted line converts the input arguments into float numbers. This way, the function will be more resilient and accept numeric values as strings even if this isn’t the expected input type.

Of course, this implementation isn’t perfect, but functionality-wise, it’s better than the first one. Don’t you think?

Readability

Code readability is one of the core principles behind Python. From the beginning, Python’s creator, Guido van Rossum, emphasized its importance, and it remains a priority for the core developers and community today. It’s even embedded in the Zen of Python:

Readability counts. (Source)

The following example shows why readability is important. Again, you’ll first have a low-quality version and then a higher-quality one.

🔴 Low-quality code:

Python
>>> def ca(w, h):
...     return w * h
...

>>> ca(12, 20)
240

This function works. It takes two numbers and multiplies them, returning the result. But can you tell what this function is for? Consider the improved version below.

Higher-quality code:

Python
>>> def calculate_rectangle_area(width: float, height: float) -> float:
...     return width * height
...

>>> calculate_rectangle_area(12, 20)
240

Now, when you read the function’s name, you immediately know what the function is about because the argument names provide additional context.

Documentation

Documenting code is a task that gets little love among software developers. However, clear and well-structured documentation is essential for evaluating the quality of any software project. Below is an example of how documentation can contribute to code quality.

🔴 Low-quality code:

Python
>>> def multiply(a, b):
...     return a * b
...

>>> multiply(2, 3)
6

This function provides no explanation of parameters or return values. If you dig into the code, then you can tell what the function does, but it would be nice to have some more context. That’s where documentation comes in. The improved version below uses docstrings and type hints as ways to document the code.

Higher-quality code:

Python
>>> def multiply(a: float, b: float) -> float:
...     """Multiply two numbers.
...     Args:
...         a (float): First number.
...         b (float): Second number.
...     Returns:
...         float: Product of a and b.
...     """
...     return a * b
...

>>> multiply(2, 3)
6

In the function’s docstring, you provide context that lets others know what the function does and what type of input it should take. You also specify its return value and corresponding data type.

Compliance

Meeting the requirements of well-known and widely accepted code standards is another key factor that influences the quality of a piece of code. The relevant standards will vary depending on the project at hand. A good generic example is writing Python code that follows the standards and conventions established in PEP 8, the official style guide for Python code. Here’s an example of low-quality code that doesn’t follow PEP 8 guidelines.

🔴 Low-quality code:

Python
>>> def calcTotal(price,taxRate=0.05): return price*(1+taxRate)
...

>>> calcTotal(1.99)
2.0895

This function doesn’t follow the naming conventions and spacing norms established in PEP 8. The code might work, but it doesn’t look like quality Python code. It isn’t Pythonic. Now for the improved version.

Higher-quality code:

Python
>>> def calculate_price_with_taxes(
...     base_price: float, tax_rate: float = 0.05
... ) -> float:
...     return base_price * (1 + tax_rate)
...

>>> calculate_total_price(1.99)
2.0895

Here, the function sticks to the recommended convention of using snake case for function and variable names. It also uses proper spacing between symbols and a consistent line length policy.

Reusability

Reusability is also a fundamental characteristic of high-quality code. Reusable code reduces repetition, which improves maintainability and has a strong impact on productivity. Consider the following toy example that illustrates this quality factor.

🔴 Low-quality code:

Python
>>> def greet_alice():
...     return "Hello, Alice!"
...

>>> greet_alice()
'Hello, Alice!'

This function hardcodes its use case. It only works when you want to greet Alice, which is pretty restrictive. Check out the enhanced version below.

Higher-quality code:

Python
>>> def greet(name: str) -> str:
...     return f"Hello, {name}!"
...

>>> greet("Alice")
'Hello, Alice!'
>>> greet("John")
'Hello, John!'
>>> greet("Jane")
'Hello, Jane!'

Although quite basic, this function is more generic and useful than the previous version. It takes a person’s name as an argument and builds a greeting message using an f-string. Now, you can greet all your friends!

Maintainability

Maintainability is all about writing code that you or other people can quickly understand, update, extend, and fix. Avoiding repetitive code and code with multiple responsibilities are key principles to achieving this quality characteristic. Take a look at the example below.

🔴 Low-quality code:

Python
>>> def process(numbers):
...     cleaned = [number for number in numbers if number >= 0]
...     return sum(cleaned)
...

>>> print(process([1, 2, 3, -1, -2, -3]))
6

Even though this function is pretty short, it has multiple responsibilities. First, it cleans the input data by filtering out negative numbers. Then, it calculates the total and returns it to the caller. Now, take a look at the improved version below.

Higher-quality code:

Python
>>> def clean_data(numbers: list[int]) -> list[int]:
...     return [number for number in numbers if number >= 0]
...

>>> def calculate_total(numbers: list[int]) -> int:
...     return sum(numbers)
...

>>> cleaned = clean_data([1, 2, 3, -1, -2, -3])
>>> print(calculate_total(cleaned))
6

This time, you have a function that cleans the data and a second function that calculates the total. Each function has a single responsibility, so they’re more maintainable and easier to understand.

Robustness

Writing robust code is also fundamental in Python or any other language. Robust code is capable of handling errors gracefully, preventing crashes and unexpected behaviors and results. Check out the example below, where you code a function that divides two numbers.

🔴 Low-quality code:

Python
>>> def divide_numbers(a, b):
...     return a / b
...

>>> divide_numbers(4, 2)
2.0
>>> divide_numbers(4, 0)
Traceback (most recent call last):
    ...
ZeroDivisionError: division by zero

This function divides two numbers, as expected. However, when the divisor is 0, the code breaks with a ZeroDivisionError exception. To fix the issue, you need to properly handle the exception.

Higher-quality code:

Python
>>> def divide_numbers(a: float, b: float) -> float | None:
...     try:
...         return a / b
...     except ZeroDivisionError:
...         print("Error: can't divide by zero")
...

>>> divide_numbers(4, 2)
2.0
>>> divide_numbers(4, 0)
Error: can't divide by zero

Now, your function handles the exception, preventing a code crash. Instead, you print an informative error message to the user.

Testability

You can say that a piece of code is testable when it allows you to quickly write and run automated tests that check the code’s correctness. Consider the toy example below.

🔴 Low-quality code:

Python
def greet(name):
    print(f"Hello, {name}!")

This function is hard to test because it uses the built-in print() function instead of returning a concrete result. The code operates through a side effect, making it more challenging to test. For example, here’s a test that takes advantage of pytest:

Python
import pytest

def test_greet(capsys):
    greet("Alice")
    captured = capsys.readouterr()
    assert captured.out.strip() == "Hello, Alice!"

This test case works. However, it’s hard to write because it demands a relatively advanced knowledge of the pytest library.

You can replace the call to print() with a return statement to improve the testability of greet() and simplify the test.

Higher-quality code:

Python
def greet(name: str) -> str:
    return f"Hello, {name}!"

def test_greet():
    assert greet("Alice") == "Hello, Alice!"

Now, the function returns the greeting message. This makes the test case quicker to write and requires less knowledge of pytest. It’s also more efficient and quick to run, so this version of greet() is more testable.

Efficiency

Efficiency is another essential factor to take into account when you have to evaluate the quality of a piece of code. In general, you can think of efficiency in terms of execution speed and memory consumption.

Depending on your project, you may find other features that could be considered for evaluating efficiency, including disk usage, network latency, energy consumption, and many others.

Consider the following code that computes the Fibonacci sequence of a series of numbers using a recursive algorithm.

🔴 Low-quality code:

Python efficiency_v1.py
from time import perf_counter

def fibonacci_of(n):
    if n in {0, 1}:
        return n
    return fibonacci_of(n - 1) + fibonacci_of(n - 2)

start = perf_counter()
[fibonacci_of(n) for n in range(35)]  # Generate 35 Fibonacci numbers
end = perf_counter()

print(f"Execution time: {end - start:.2f} seconds")

Go ahead and run this script from your command line:

Shell
$ python efficiency_v1.py
Execution time: 1.83 seconds

The execution time is almost two seconds. Now consider the improved implementation below.

Higher-quality code:

Python efficiency_v2.py
from time import perf_counter

cache = {0: 0, 1: 1}

def fibonacci_of(n):
    if n in cache:
        return cache[n]
    cache[n] = fibonacci_of(n - 1) + fibonacci_of(n - 2)
    return cache[n]

start = perf_counter()
[fibonacci_of(n) for n in range(35)]  # Generate 35 Fibonacci numbers
end = perf_counter()

print(f"Execution time: {end - start:.2f} seconds")

This implementation optimizes the Fibonacci computation using caching. Now, run this improved code from the command line again:

Shell
$ python efficiency_v1.py
Execution time: 0.01 seconds

Wow! This code is quite a bit faster than its previous version. You’ve improved the code’s efficiency by boosting performance.

Scalability

The scalability of a piece of code refers to its ability to handle increasing workload, data size, or user demands without compromising the code’s performance, stability, or maintainability. It’s a relatively complex concept, so to illustrate it, here’s a quick example of code that deals with increasing data size.

🔴 Low-quality code:

Python
>>> def sum_even_numbers(numbers):
...     even_numbers = [number for number in numbers if number % 2 == 0]
...     return sum(even_numbers)
...

>>> sum_even_numbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
30

This function filters the odd numbers out of the input list, creating a new list with all the values in memory. This can become an issue when the size of the input list grows considerably. To make the code scale well when the input data grows, you can replace the list comprehension with a generator expression.

Higher-quality code:

Python
>>> def sum_even_numbers(numbers):
...     return sum(number for number in numbers if number % 2 == 0)
...

>>> sum_even_numbers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
30

The generator expression that you use as the argument to sum() ensures that only one value is stored in memory while calculating the total.

Security

Secure code prevents security vulnerabilities, protects sensitive data, and defends against potential attacks or malicious inputs. Following best practices ensures that systems stay safe, reliable, and resilient against common security threats.

A good example of risky code is when you accept a user’s input without validating it, and use this input for further processing.

🔴 Low-quality code:

Python input_v1.py
user_input = "Amount to withdraw? "
amount = int(input(user_input))
available_balance = 1000
print(f"Here are your {amount:.2f}USD")
print(f"Your available balance is {available_balance - amount:.2f}USD")

This code uses the built-in input() function to grab the user input. The user should provide the amount of money to withdraw:

Shell
$ python input_v1.py
Amount to withdrawn? 300
Here are your 300.00USD
Your available balance is 700.00USD

The script takes the input data, simulates a cash withdrawal, and computes the final balance. Now, say that you enter the following:

Shell
$ python input_v1.py
Amount to withdrawn? 2000
Here are your 2000.00USD
Your available balance is -1000.00USD

In this case, the code has an error because the input value is greater than the available amount, and there’s no validation in place. As a result, the code gives out more money than it should. While this may seem like something that wouldn’t happen in the real world, it’s a simple example of a security flaw.

Higher-quality code:

Python
user_input = "Amount to withdraw? "
amount = int(input(user_input))
available_balance = 1000
if amount > available_balance:
    print("Insufficient funds")
    amount = 0
else:
    print(f"Here are your {amount:.2f}USD")

print(f"Your available balance is {available_balance - amount:.2f}USD")

In this updated version of the code, you ensure that the user has provided a valid amount by using a conditional statement to check the available balance.

Managing Trade-Offs Between Code Quality Characteristics

While writing your code, you’ll often encounter trade-offs between different code quality characteristics. In these situations, you should aim to find a good balance. Certain decisions prioritize one characteristic at the expense of another depending on project goals, constraints, and requirements.

Here are some common conflicts between code quality characteristics:

Conflict Description
Readability vs Efficiency Writing highly-optimized code for processing a dataset may make the code harder to read than using a basic loop.
Maintainability vs Performance Using multiple small functions can introduce function call overhead compared to an inlined and optimized solution appearing in different places.
Testability vs Efficiency Adding mock dependencies or extensive logging to make code testable might slow down performance.
Scalability vs Readability Distributed systems or parallelized code can be harder to read or understand than a simple sequential implementation.
Reusability vs Maintainability Creating highly generic and reusable components might add unnecessary complexity to a project.
Compliance vs Performance Following PEP 8 and using type hinting may introduce extra verbosity and minor overhead.
Robustness vs Readability Adding extensive error handling, such as tryexcept blocks and logging can clutter the codebase.

To balance these conflicts, you should consider some of the following factors:

  • Project requirements: What is the primary goal?
  • Team skills: Who will maintain the code?
  • Development stage: Is the project in an early stage or already in production?
  • Use context: What is the usage domain of your code?

Depending on the requirements for your code, you can make a decision on what characteristics to favor over others. For example, if you’re working in a performance-critical system, then you’ll likely favor efficiency over readability and maintainability. If you’re working in a financial system, then you might favor security over other characteristics.

Applying Best Practices for High-Quality Code in Python

There are many things to consider on your journey to achieving high-quality Python code. First, this journey isn’t one of pure objectivity. There could be some strong feelings and opinions about what high-quality code is.

While most Python developers may agree on the key characteristics of quality code, their approaches to achieving them will vary. The most debated topics usually come up when you talk about readability, maintainability, and scalability because these are difficult to measure objectively.

Here’s a table that summarizes the strategies that you can follow to write high-quality Python code:

Characteristic Strategy Resources Tools and Techniques
Functionality Adhere to the requirements Requirement documents Text processors and spreadsheet apps
Readability Use descriptive names for code objects PEP 8, code reviews Code reviews, AI assistants
Documentation Use docstrings, inline comments, good README files, and external documentation PEP 257, README guide Sphinx, mkdocs, AI assistants
Compliance Follow relevant standards, use a coding style guide consistently, use linters, formatters, and static type checkers PEP 8 black, isort, flake8, pylint, ruff, mypy
Reusability Write parameterized and generic functions Design principles (SOLID), Refactoring techniques Code reviews, AI assistants
Maintainability Write modular code, apply the don’t repeat yourself (DRY) and separation of concerns (SoC) principles Design patterns, code refactoring techniques pylint, flake8, ruff
Robustness Do proper error handling and input validation Exception handling guides pylint, ruff
Testability Write unit tests and use test automation Code testing guides pytest, unittest, doctest, tox, nox, AI assistants
Efficiency Use appropriate algorithms and data structures Algorithms, Big O notation, data structures cProfile, line_profiler, timeit, perf
Scalability Use modular structures Software architecture topics Code reviews, AI assistants
Security Sanitize inputs and use secure defaults and standards Security best practices bandit, safety

Of course, this table isn’t an exhaustive summary of strategies, techniques, and tools for achieving code quality. However, it provides a good starting point.

In the following sections, you’ll find more detailed information about some of the strategies, techniques, and tools listed in the table above.

Style Guides

A coding style guide defines guidelines for a consistent way to write code. Typically, these guidelines are primarily cosmetic, meaning they don’t change the logical outcome of the code, although some stylistic choices do prevent common logical mistakes. Following a style guide consistently facilitates making code readable, maintainable, and scalable.

For most Python code, you have PEP 8, the official and recommended coding style guide. It contains coding conventions for Python code and is widely used by the Python community. It’s a great place to start since it’s already well-defined and polished. To learn more about PEP 8, check out the How to Write Beautiful Python Code With PEP 8 tutorial.

You’ll also find that companies dedicated to developing software with Python may have their own internal style guide. That’s the case with Google, which has a well-crafted coding style guide for their Python developers.

Other than that, some Python projects have established their own style guides with guidelines specific to contributors. A good example is the Django project, which has a dedicated coding style guide.

Code Reviews

A code review is a process where developers examine and evaluate each other’s code to ensure it meets quality standards before merging it into the main or production codebase. The process may be carried out by experienced developers who review the code of other team members.

A code reviewer may have some of the following responsibilities:

  • Verifying the code works as expected
  • Ensuring the code is clear, with meaningful variable names and proper comments
  • Catching repetitive code, encouraging modular design, and promoting reusable functions
  • Identifying logic errors, edge cases, and incorrect assumptions
  • Ensuring the code properly handles errors, edge cases, and exceptional situations
  • Confirming the code adheres to style guidelines
  • Detecting vulnerabilities like lack of input validation or unsafe usage of functions and objects
  • Spotting inefficient algorithms or resource-intensive operations
  • Ensuring the code has tests and satisfactory test coverage

As you can conclude, a good code review involves checking for code correctness, readability, maintainability, security, and many other quality factors.

To conduct a code review, a reviewer typically uses a specialized platform like GitHub, which allows them to provide relevant feedback in the form of comments, code change suggestions, and more.

Code reviews are vital for producing high-quality Python code. They improve the code being reviewed and help developers grow, learn, and collectively promote coding standards and best practices across the team.

AI Assistants

Artificial intelligence (AI) and large language models (LLMs) are getting a lot of attention these days. These tools have a growing potential as assistants that can help you write, review, improve, extend, document, test, and maintain your Python code.

AI assistants enhance code quality by offering you help throughout the software development process. With the help of an AI assistant, you can do some of the following:

  • Generate code snippets based on descriptions
  • Get context-aware code completion
  • Check for typos and syntax errors
  • Analyze code for readability
  • Ensure the code follows the code style guide
  • Make the code maintainable by applying best practices
  • Identify potential bugs and logic errors
  • Understand error messages and get quick fixes
  • Simplify complex code
  • Reduce or remove repetition
  • Use Pythonic approaches and practices
  • Generate docstrings, comments, README files, and documentation
  • Write unit tests for your code and ensure good test coverage
  • Detect common security vulnerabilities and issues
  • Identify inefficient algorithms and write faster alternatives

AI assistants and LLMs can also act as interactive mentors for junior developers. To explore how to use AI tools to improve the quality of your code, you can check out the following resources:

You can make it your goal to use AI to improve both your code quality and general development workflow. Remember that while AI may not replace you, those who learn to use it effectively will have an advantage.

Documentation Tools

To document your Python code, you can take advantage of several available tools. Before taking a look at some of them, it’s important to mention PEP 257, which describes conventions for Python’s docstrings.

A docstring is typically a triple-quoted string that you write at the beginning of modules, functions, and classes. Their purpose is to provide the most basic layer of code documentation. Modern code editors and integrated development environments (IDE) take advantage of docstrings to display context help when you’re working on your code.

As a bonus, there are a few handy tools to generate documentation directly from the code by reading the docstrings. To learn about a couple of these tools, check out the following resources:

Probably the most common piece of documentation that you’d write for a Python project is a README file. It’s a document that you typically add to the root directory of a software project, and is often a short guide that provides essential information about the project. To dive deeper into best practices and tools for writing a README, check out the Creating Great README Files for Your Python Projects tutorial.

You can also take advantage of AI, LLMs, and chatbots to help you generate detailed and high-quality documentation for your Python code. To experiment with this possibility, check out the Document Your Python Code and Projects With ChatGPT tutorial.

Code Linters

The Python community has built a few tools, known as linters, that you can set up and use to check different aspects of your code. Linters analyze code for potential errors, code smells, and adherence to coding style guides like PEP 8. They check for:

  • Syntax errors: Detect basic syntax issues, such as missing colons and commas, invalid variable usage, and similar.
  • Coding style violations: Check for correct indentation, spacing, and naming conventions.
  • Unused imports, variables, and names: Flag unnecessary code that can be removed.
  • Code complexity: Warn about overly complex functions and excessively long methods or classes.
  • Potential bugs: Detect issues like shadowed variables and names, unreachable or dead code, and incorrect method and function signatures.
  • Error handling issues: Identify overly broad exceptions and empty except blocks.
  • Undocumented code: Check for missing docstrings in modules, classes, functions, and so on.
  • Best practices: Advise against poor coding practices, such as mutable default argument values.
  • Security vulnerabilities: Warn against insecure coding practices, such as hardcoded passwords, exposed API tokens, and the use of eval() and exec().
  • Code duplication: Alert when similar code blocks appear multiple times.

Here are some modern Python linters with brief descriptions:

Linter Description
Pylint A linter that checks for errors, enforces PEP 8 coding style standards, detects code smells, and evaluates code complexity.
Flake8 A lightweight linter that checks for PEP 8 compliance, syntax errors, and common coding issues. It can be extended with plugins.
Ruff A fast, Rust-based tool that provides linting, code formatting, and type checking. It aims to be a drop-in replacement for Flake8 and Black.

To run these linters against your code, you use different commands depending on the tool of choice. Fortunately, most of these tools can be nicely integrated into the majority of Python code editors and IDEs, such as Visual Studio Code and PyCharm.

These editors and IDEs have the ability to run linters in the background as you type or save your code. They typically highlight, underline, or otherwise identify problems in the code before you run it. Think of this feature as an advanced spell-check for code.

Static Type Checkers

Static type checkers are another category of code checker tools. Unlike linters, which cover a wide range of issues and possible issues, static type checkers focus on validating type annotations or type hints. They catch possible type-related bugs without running the code.

Some aspects of your code that you can check with static type checkers include the following:

  • Incorrect type usage in variables and function calls: Detect variables used with an incompatible type or function called with arguments of wrong types.
  • Missing or incorrect annotations: Warn if a piece of code lacks proper type hints.
  • Type inference errors: Identify places where inferred types don’t match expectations.
  • Issues with None: Catch potential NoneType attribute errors.
  • Type compatibility in collections: Ensure lists, tuples, dictionaries, and other collections contain the correct data types.
  • Protocol and interface compliance: Verify that classes correctly implement protocols and interfaces.

Here are a couple of modern static type checkers for Python code:

Type Checker Description
mypy A static type checker that verifies type hints and helps catch type-related errors before runtime.
Pyright A fast static type checker and linter, optimized for large codebases and also used in VS Code’s Python extension.

Again, you can install and run these tools against your code from the command line. However, modern code editors and IDEs often have them built-in or available as extensions. Once you have the type checker integrated, your editor will show you visual signals that point to issues flagged by the checker.

Code Formatters

A code formatter is a tool that automatically parses your code and formats it to be consistent with the accepted coding style. Usually, the standard path is to use PEP 8 as the style reference. Running a code formatter against your code produces style-related changes, including the following:

  • Consistent indentation: Converts all indentation to a standard indentation of four spaces.
  • Line length: Wraps lines exceeding length limits.
  • Spacing and padding: Adds or removes spaces for readability, such as around operators and after commas.
  • String quotes consistency: Converts all strings to a single quote or double quote style.
  • Import sorting: Ensures imports are alphabetically ordered and grouped logically.
  • Consistent use of blank lines: Ensures blank lines between functions and classes.

Here are some code formatting tools:

Formatter Description
Black Formats Python code without compromise
Isort Formats imports by sorting alphabetically and separating into sections
Ruff Provides linting, code formatting, and import sorting

You can install and run these code formatters against your code from the command line. Again, you’ll find these tools integrated into code editors and IDEs, or you’ll have extensions to incorporate them into your workflow.

Typically, these tools will apply the formatting when you save the changes, but they also have options to format the code on paste, type, and so on.

If you work on a team of developers, then you can configure everyone’s environment to use the same code formatter with the same defaults and rely on the automated formatting. This frees you from having to flag formatting issues in your code reviews.

Code Testing

Writing and consistently running tests against your code help ensure correctness, reliability, and maintainability. Well-structured tests can significantly improve code quality by catching bugs early, enforcing best practices, and making code easier to refactor without breaking its functionality.

Tests can improve the quality of your code in some of the following ways:

  • Ensuring the code works as expected: Running tests confirms that a function, class, or module behaves correctly under different conditions.
  • Catching bugs early: Tests help identify logic errors, unexpected behavior, and edge cases.
  • Facilitating code refactoring: Having tests in place allows safe refactoring, letting you verify that changes don’t break existing functionality.
  • Improving readability and design: Writing tests forces you to think about the names of objects and the modularity of your code, leading to readable, reusable, and better-organized code.
  • Preventing regression issues: Regression tests catch unintended side effects when modifying existing code.
  • Increase confidence in deployments: Automating tests ensures that every release is stable, functional, and meets requirements.

In Python, you have several tools to help you write and automate tests. You can use doctest and unittest from the standard library, or you can use the pytest third-party library. To learn about these tools and how to test your code with them, check out the following resources:

As a bonus, you can also use an AI assistant to help you write tests for your Python code. These tools will dramatically improve your productivity. Check out the Write Unit Tests for Your Python Code With ChatGPT tutorial for a quick look at the topic.

Code Profilers

Code profilers analyze a program’s runtime behavior. They measure performance-related metrics, such as execution speed, memory usage, CPU usage, and function call frequencies. A code profiler flags the parts of your code that consume the most resources, allowing you to make changes to optimize the code’s efficiency.

Running code profiling tools on your code allows you to:

  • Identify performance bottlenecks like slow functions or loops that impact execution time.
  • Detect intensive CPU usage or high memory consumption, letting you optimize resource usage
  • Spot inefficient algorithms, allowing you to replace inefficient code with optimized solutions.
  • Detect high latency, letting you speed up operations like database queries, API calls, or computations.
  • Find scalability issues related to handling larger datasets or higher loads.
  • Make good refactoring decisions based on detailed data and statistics.

Common Python profiling tools include the following:

Tool Description
cProfile Measures function execution times
timeit Times small code snippets
perf Finds hot spots in code, libraries, and even the operating system’s code
py-spy Finds what a program is spending time on without restarting it or modifying its code in any way
Scalene Detects performance bottlenecks and memory leaks

You can install and run these tools against your code to find opportunities to improve the code efficiency. Ultimately, the choice of a profiler depends on your specific use case. You can use cProfile and timeit from the Python standard library for quick checks that don’t require installing third-party libraries.

If you’re using Linux, you can try perf, a powerful performance profiling tool now supported in Python 3.12. To learn more about perf, check out the Python 3.12 Preview: Support For the Linux perf Profiler tutorial.

Finally, py-spy and Scalene are third-party libraries that you can consider using when you want to profile your code using a Python package that you can install from the Python Package Index (PyPI).

Vulnerability Checkers

It’s possible to inadvertently leak sensitive data, such as user credentials and API keys, through your code or expose your code to other security vulnerabilities like SQL injection attacks. To reduce the risk of such security incidents, you should perform security or vulnerability scanning on your Python code.

Bandit is a security-focused linter that scans for common vulnerabilities and insecure coding patterns in Python code. Some of these patterns include the use of:

  • Unsanitized user inputs
  • The exec(), eval(), and pickle.load() functions
  • The assert statement
  • Hardcoded API keys or passwords
  • Insecure file operations
  • Shell commands
  • Unencrypted HTTP requests instead of HTTPS
  • Broad exceptions
  • Hardcoded SQL queries

These are just a sample of the tests that Bandit can run against your code. You can even use it to write your own tests and then run them on your Python projects. Once you have a list of detected issues, you can take action and fix them to make the code more secure.

Deciding When to Check the Quality of Your Code

You should check the quality of your code frequently. If automation and consistency aren’t there, it’s possible for a large team or project to lose sight of the goal and start writing lower-quality code. It can happen slowly, one tiny issue here, another there. Over time, all those issues pile up and eventually, you can end up with a codebase that’s buggy, hard to read, hard to maintain, and a pain to extend with new features.

To avoid falling into the low-quality spiral, check your code often! For example, you can check the code when you:

  • Write it
  • Commit it
  • Test it

You can use code linters, static type checkers, formatters, and vulnerability checkers as you write code. To avoid additional workload, you should take the time to configure your development environment so that these tools run automatically while you write and save the code. It’s generally a matter of finding the plugin or extension for your IDE or editor of choice.

You probably use a version control system (VCS) to manage your code. Most of these systems have features that let you run scripts, tests, and other checks before committing the code. You can use these capabilities to block any new code that doesn’t meet your quality standards.

While this may seem drastic, enforcing quality checks for every bit of code is an important step toward ensuring stability and high quality. Automating the process at the front gate to your code is a good way to avoid low-quality code.

Finally, when you have a good battery of tests and run them frequently, you may have failing tests after you add new code or make changes and fixes. These failed tests demand code corrections, and are great opportunities to improve the code’s quality. Test-driven development is also a good strategy to ensure code quality.

Conclusion

You’ve learned about the different aspects that define the quality of Python code, ranging from basic functionality to advanced characteristics such as readability, maintainability, and scalability.

You learned that to produce high-quality code, you should adhere to coding standards and employ tools such as linters, type checkers, and formatters. You also delved into strategies and techniques such as code reviews, testing, and using AI assistants.

Writing high-quality code is crucial for you as a Python developer. High-quality code reduces development costs, minimizes errors, and facilitates collaboration between coworkers.

In this tutorial, you’ve learned how to:

  • Identify characteristics of low and high-quality code
  • Implement best practices to enhance code readability, maintainability, and efficiency
  • Use tools like linters, type checkers, and formatters to enforce code quality
  • Use effective code reviews and AI assistance to achieve code quality
  • Apply testing and profiling techniques to ensure code robustness and performance

With this knowledge, you can now confidently write, review, and maintain high-quality Python code, which will lead to a more efficient development process and robust applications.

Frequently Asked Questions

Now that you have some experience with enhancing Python code quality, you can use the questions and answers below to check your understanding and recap what you’ve learned.

These FAQs are related to the most important concepts you’ve covered in this tutorial. Click the Show/Hide toggle beside each question to reveal the answer.

You can check code quality in Python by using tools like linters and static type checkers to ensure adherence to coding standards, and detect potential errors and bad practices.

High-quality Python code is characterized by readability, maintainability, scalability, robust error handling, and adherence to standards like PEP 8.

You can write high-quality Python code by following best practices like descriptive naming, modular design, comprehensive testing, and using tools to enforce coding standards.

You can use linters like Pylint, code formatters like Black and Ruff, static type checkers like mypy, and security analyzers like Bandit, to improve the quality of your Python code.

Take the Quiz: Test your knowledge with our interactive “Python Code Quality: Best Practices and Tools” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Python Code Quality: Best Practices and Tools

In this quiz, you'll test your understanding of Python code quality, tools, and best practices. By working through this quiz, you'll revisit the importance of producing high-quality Python code that's functional, readable, maintainable, efficient, and secure.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Leodanis Pozo Ramos

Leodanis is an industrial engineer who loves Python and software development. He's a self-taught Python developer with 6+ years of experience. He's an avid technical writer with a growing number of articles published on Real Python and other sites.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!