Python Code Quality: Best Practices and Tools (original) (raw)

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:

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:


Python Code Quality Illustration

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

Go ahead and run this script from your command line:

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

Higher-quality code:

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

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:

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:

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:

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

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

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:

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 try … except blocks and logging can clutter the codebase.

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

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:


Python Code Quality Illustration

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.