What Is Clean Code? A Guide to Principles and Best Practices
Writing code is like giving a speech. If you use too many big words, you confuse your audience. Define every word, and you end up putting your audience to sleep.
Similarly, when you write code, following clean code rules makes sure it is readable, understandable, and maintainable for future readers. To paraphrase software engineer Martin Fowler, "Anybody can write code that a computer can understand. Good programmers write code that humans can understand."
As software developers, understanding how to write clean code that is functional, easy to read, and adheres to best practices helps you create better software consistently.
This article discusses what clean code is and why it's essential. It also provides principles and best practices for writing clean and maintainable code.
What Is Clean Code?
Clean code is a term used to refer to code that is easy to read, understand, and maintain. It was made popular by Robert Cecil Martin, also known as Uncle Bob, who wrote "Clean Code: A Handbook of Agile Software Craftsmanship" in 2008. In this book, he presented a set of principles and best practices for writing clean code, such as using meaningful names, short functions, clear comments, and consistent formatting.
Ultimately, the goal of clean code is to create software that is not only functional but also readable, maintainable, and efficient throughout its lifecycle.
Why Is Clean Code Important?
When teams adhere to clean code principles, the codebase is easier to read and navigate, which makes it faster for developers to get up to speed and start contributing. Here are some reasons why clean code is essential.
- Readability and maintenance: Following clean code practices and clean coding rules prioritizes clarity, which makes reading, understanding, and modifying code easier. Writing readable code reduces the time required to grasp the code's functionality, leading to faster development times.
- Team collaboration: Clear and consistent code facilitates communication and cooperation among team members. By adhering to established coding standards and writing readable code, developers easily understand each other's work and collaborate more effectively.
- Debugging and issue resolution: Clean code is designed with clarity and simplicity in mind, making it easier to locate and understand specific sections of the codebase. Clear structure, meaningful variable names, and well-defined functions make it easier for developers to identify and resolve issues. Recent research even suggests that AI models struggle more with poorly structured code, making clean code practices essential not only for humans but also for AI-assisted development.
- Improved quality and reliability: Clean code prioritizes adherence to established coding standards and well-structured code. This reduces the risk of introducing errors, leading to higher-quality and more reliable software down the line.
Now that we understand why clean code is essential, let's delve into some best practices and principles to help you write clean code.
Turn clean code best practices into automated checks. Codacy's free IDE extension analyzes your repository, highlights issues, and helps your team apply fixes quickly.
Principles of Clean Code
Like a beautiful painting needs the right foundation and brushstrokes, well-crafted code requires adherence to specific principles. These principles help developers write code that is clear, concise, and, ultimately, a joy to work with.
Let's dive in.
1. Avoid Hard-Coded Numbers
Use named constants instead of hard-coded values. Write constants with meaningful names that convey their purpose. This improves clarity and makes it easier to modify the code.
Example:
The example below uses the hard-coded number 0.1 to represent a 10% discount. This makes it difficult to understand the meaning of the number (without a comment) and adjust the discount rate if needed in other parts of the function.
Before:
discount = price * 0.1 # 10% discount
return price - discount
A cleaner way to write this is to replace the hard-coded number with a named constant TEN_PERCENT_DISCOUNT. The name instantly conveys the meaning of the value, making the code more self-documenting.
After :
TEN_PERCENT_DISCOUNT = 0.1
discount = price * TEN_PERCENT_DISCOUNT
return price - discount
Also, if the discount rate needs to be changed, it only requires modifying the constant declaration, not searching for multiple instances of the hard-coded number.
2. Use Meaningful and Descriptive Names
Choose names for variables, functions, and classes that reflect their purpose and behavior. This makes the code self-documenting and easier to understand without extensive comments.
As Robert Martin puts it, “A name should tell you why it exists, what it does, and how it is used. If a name requires a comment, then the name does not reveal its intent.”
Example:
If we take the code from the previous example, it uses generic names like "price" and "discount," which leaves their purpose ambiguous. Names like "price" and "discount" could be interpreted differently without context.
Before:
TEN_PERCENT_DISCOUNT = 0.1
discount = price * TEN_PERCENT_DISCOUNT
return price - discount
Instead, you can declare the variables to be more descriptive.
After:
TEN_PERCENT_DISCOUNT = 0.1
discount_amount = product_price * TEN_PERCENT_DISCOUNT
return product_price - discount_amount
This improved code uses specific names like "product_price" and "discount_amount," providing a clearer understanding of what the variables represent and how we use them.
3. Use Comments Sparingly, and When You Do, Make Them Meaningful
You don't need to comment on obvious things. Excessive or unclear comments can clutter the codebase and become outdated, leading to confusion and a messy codebase.
Example:
Before:
# This function groups users by id
# ... complex logic ...
# ... more code …
The comment about the function is redundant and adds no value. The function name already states that it groups users by id; there's no need for a comment stating the same.
Instead, use comments to convey the "why" behind specific actions or explain behaviors.
After:
"""Groups users by id to a specific category (1-9).
Warning: Certain characters might not be handled correctly.
Please refer to the documentation for supported formats.
Args:
user_id (str): The user id to be grouped.
Returns:
int: The category number (1-9) corresponding to the user id.
Raises:
ValueError: If the user id is invalid or unsupported.
"""
# ... complex logic ...
# ... more code …
This comment provides meaningful information about the function's behavior and explains unusual behavior and potential pitfalls.
4. Write Short Functions That Only Do One Thing
Follow the single responsibility principle (SRP), which means that a function should have one purpose and perform it effectively. Functions are more understandable, readable, and maintainable when they have only one job. It also makes testing them very easy.
If a function becomes too long or complex, consider breaking it into smaller, more manageable functions.
Example:
Before:
# ... validate users...
# ... calculate values ...
# ... format output …
This function performs three tasks: validating users, calculating values, and formatting output. If any of these steps fail, the entire function fails, making debugging more complex. Also, if we need to change the logic of one task only, we risk introducing unintended side effects in another task.
Instead, try to assign each task a function that does just one thing.
After:
# ... data validation logic ...
def calculate_values(data):
# ... calculation logic based on validated data ...
def format_output(data):
# ... format results for display …
The improved code separates the tasks into distinct functions. This results in more readable, maintainable, and testable code. Additionally, if a change needs to be made, it will be easier to identify and modify the specific function responsible for the desired functionality.
5. Follow the DRY (Don't Repeat Yourself) Principle and Avoid Duplicating Code or Logic
Avoid writing the same code more than once. Instead, reuse your code using functions, classes, modules, libraries, or other abstractions. This makes your code more efficient, consistent, and maintainable. It also reduces the risk of errors and bugs, as you only need to modify your code in one place when you update it.
Example:
Before:
return quantity * price
def calculate_laptop_price(quantity, price):
return quantity * price
In the above example, both functions calculate the total price using the same formula. This violates the DRY principle.
We can fix this by defining a single calculate_product_price function that we use for books and laptops. This reduces code duplication and helps improve the maintenance of the codebase.
After:
return product_quantity * product_price
6. Follow Established Code-Writing Standards
Know your programming language's conventions for spacing, comments, and naming. Most programming languages have community-accepted coding standards and style guides, such as PEP 8 for Python and Google JavaScript Style Guide for JavaScript.
Here are some specific examples:
- Java:
- Use camelCase for variable, function, and class names.
- Indent code with four spaces.
- Put opening braces on the same line.
- Python:
- Use snake_case for variable, function, and class names.
- Use spaces over tabs for indentation.
- Put opening braces on the same line as the function or class declaration.
- JavaScript:
- Use camelCase for variable and function names.
- Use snake_case for object properties.
- Indent code with two spaces.
- Put opening braces on the same line as the function or class declaration.
Also, consider extending some of these standards by creating internal coding rules for your organization. This can include information on creating and naming folders, or on describing function names within your organization.
7. Encapsulate Nested Conditionals into Functions
One way to improve the readability and clarity of functions is to encapsulate nested if/else statements into other functions. Encapsulating such logic into a function with a descriptive name clarifies its purpose and simplifies code comprehension. In some cases, it also makes it easier to reuse, modify, and test the logic without affecting the rest of the function.
In the code sample below, the discount logic is nested within the calculate_product_discount function, making it difficult to understand at a glance.
Example:
Before:
discount_amount = 0
if product_price > 100:
discount_amount = product_price * 0.1
elif price > 50:
discount_amount = product_price * 0.05
else:
discount_amount = 0
final_product_price = product_price - discount_amount
return final_product_price
We can clean this code up by separating the nested if/else condition that calculates discount logic into another function called get_discount_rate, and then calling the get_discount_rate in the calculate_product_discount function. This makes it easier to read.
get_discount_rate is now isolated and can be reused by other functions in the codebase. It’s also easier to change, test, and debug it without affecting the calculate_discount function.
After:
discount_rate = get_discount_rate(product_price)
discount_amount = product_price * discount_rate
final_product_price = product_price - discount_amount
return final_product_price
def get_discount_rate(product_price):
if product_price > 100:
return 0.1
elif product_price > 50:
return 0.05
else:
return 0
8. Refactor Continuously
Conduct regular code reviews and refactor your code to improve its structure, readability, and maintainability. Consider the readability of your code for the next person who will work on it, and always leave the codebase cleaner than you found it.
9. Use Version Control
Version control systems meticulously track every change made to your codebase, enabling you to understand its evolution and revert to previous versions if needed. This creates a safety net for code refactoring and prevents accidental deletions or overwrites.
Use version control systems like GitHub, GitLab, and Bitbucket to track changes to your codebase and collaborate effectively with others.
Automate everything in this guide
Codacy scans your repositories for quality and security issues in real time and provides actionable suggestions, so your team can fix problems as they develop.
Maintaining Clean Code in AI-Assisted Workflows
AI coding assistants such as GitHub Copilot and Claude Code are nowadays an integral part of most developer workflows. And while these tools can make writing and formatting code a breeze, they also introduce new considerations for maintaining clean code standards.
It’s easy for LLMs to generate functionally correct code snippets, especially for self-contained tasks. However, the deliberate design decisions required for clean code principles are not always optimized for by AI models. Research evaluating LLM-generated solutions shows that even when AI-generated code passes functional tests, it can still contain hidden defects (such as security vulnerabilities and code smells). According to other research, developers using AI coding tools may be more prone to introducing unsafe code patterns, which stresses the need for automated analysis and review processes.
Real-world incidents reinforce this risk. For example, reports linked an AWS service outage to changes made by Amazon’s internal AI coding tool, which deleted and recreated part of a system environment and caused a 13-hour disruption.
In practice, AI-generated code should follow the same clean coding rules as any other contribution to the codebase. Without proper review, AI-assisted code may even introduce more maintainability issues, code smells, and even security risks, including:
- Overly long methods: LLMs aim for function, not conciseness. They often generate long functions that bundle multiple responsibilities, which makes the code harder to read and consequently maintain
- Duplicated logic: AI code logic is often functionally sound, but it can sometimes be overly verbose. Because AI models don't know the entire codebase, they often duplicate logic, which directly violates the DRY principle.
- Inconsistent overall structure: Even well-fed AI models lack the context that your engineering team has by working on your codebase. A well-designed structure is often not part of AI-generated code snippets or files, and might clash with the rest of your application. Think inconsistent variable naming, differing file organization, and mismatched function signatures.
- General unnecessary complexity: AI code can introduce redundant conditions, overly nested logic, or unnecessary abstractions. And while the code might run correctly, this added complexity might obscure the developer’s intent, making debugging and refactoring difficult.
- Hardcoded secrets: AI-generated code may accidentally include API keys, tokens, or credentials directly in source files. If committed to a repository, these secrets can be exposed and exploited.
- Insecure dependencies: AI models may suggest libraries or packages that were safe when they appeared in training data, but have since been associated with newly discovered vulnerabilities or CVEs.
While AI-assisted code can make your engineering team more productive, it does not replace the careful application of clean code principles. Developers still need to review, refactor, and structure AI-generated code thoughtfully (as they would with human-made code) to maintain readability, quality, reliability, and keep supporting team collaboration.
Write Secure, Clean Code with Confidence Using Codacy
Writing secure, clean code is more than just following rules; it’s a commitment to producing high-quality software that is robust, understandable, and built to last. If you're looking for a tool to automate code quality and security checks across your repository, check out Codacy.
Codacy goes beyond simply enforcing coding style guidelines. It provides a comprehensive suite of features that help you write better code and prevent tech debt from piling up, including:
- Quality indicators: Codacy tracks code that is unused or error-prone, as well as performance issues and rule violations, all surfaced through static code analysis. The AI reviewer adds context and highlights logic gaps or inconsistencies that standard scanners usually miss.
- Security vulnerabilities: It detects issues from SAST, outdated or insecure dependencies (SCA), hard-coded secrets, and even IaC misconfigurations. The AI reviewer plays a big role here, too, flagging risk-introducing patterns beyond what the base scanners can detect.
- Duplication metrics: Repeated code blocks are identified and quantified, so teams have a clear view of redundancy, and maintaining cleaner code becomes instantly easier.
- Complexity metrics: Functions and methods are measured for cyclomatic complexity. This way, engineers can identify overly intricate logic within the codebase and simplify it before it becomes a maintenance burden.
- Test coverage: Codacy tracks how much of your codebase is covered by tests and points out specific gaps when pushing new changes, and highlights areas where coverage is missing or weak, providing guidance for improvements.
- AI-assisted pull request review: Extends static analysis with AI-powered insights. The reviewer evaluates the full context of a pull request (including the PR description, associated Jira tickets, and the broader codebase) to flag logic flaws, potential pre-set rule mismatches, and security gaps. Codacy’s AI reviewer acts as a wrapper around the platform’s regular scanners, reducing noise, surfacing more actionable issues, and catching additional problems using broader code context.
With support for GitHub, GitLab, and Bitbucket, Codacy integrates seamlessly into your existing development workflow. Give it a try for free and get your scan results within minutes.