A Practical Guide to Clean Coding
Table of contents
- Introduction
- 1. Embrace the Power of Simplicity
- 2. Give Variables Meaningful Names
- 3. The Power of Docstrings and Comments
- 4. Build with Modularity in Mind
- 5. Automate Testing and Catch Problems Early
- 6. Master the Art of Version Control
- 7. Write Code That Tells a Story
- 8. Set the Bar with Coding Standards
- 9. Embrace the Art of Refactoring
- 10. Seek Continuous Improvement through Feedback
- Conclusion
Introduction
Remember the last time you tried to untangle that bundle of Christmas lights or wires that you swore was wrapped up ever so nicely the year before? That’s what it feels like to work with spaghetti code. The dreaded feeling of, what is this and why was it written this way? As developers, we aim for code that’s not just functional but a joy to read and maintain. In this post, I’ll share 10 expert tips to transform your code from a tangled mess into a well-organized joy to work on.
For the examples provided we will be using Python as this is the most popular growing language currently. If you disagree, I'd love to hear which one you work with the most and why you love it.
1. Embrace the Power of Simplicity
Complex problems often have simple solutions. Strive for elegance in your code without compromising functionality. Here’s a quick comparison to illustrate the point:
# Complex Code
def process(data):
# ... 50 lines of complex logic ...
# Simplified Code
def process(data):
return [transform(d) for d in data if is_valid(d)]
The goal is to write code that reads like natural language. It should be fluid and easy to understand. If breaking things up improves readability without sacrificing performance or other requirements, it’s usually worth it. Remember, years from now, you might not recall the specific thought process or knowledge you had when writing the code. As quoted from the Zen of Python, ‘Readability counts.’ So, prioritize writing code that tells a clear, comprehensible story for anyone who might read it in the future, including your future self.
2. Give Variables Meaningful Names
Clear variable names are like signposts in your code. They guide anyone who reads your code, including your future self, towards understanding its purpose and functionality. As the Zen of Python states, ‘Explicit is better than implicit.’ This principle encourages us to be transparent and straightforward in our code.
Consider this before-and-after example:
# Before
def p(r):
return 3.14 * r * r
# After
def calculate_circle_area(radius):
PI = 3.14
return PI * radius * radius
In the ‘before’ example, it’s not immediately clear what p
and r
represent. But in the ‘after’ example, the function and variable names clearly indicate that we’re calculating the area of a circle given a radius.
Remember, your code is a story, and clear variable names are the narrators of that story. They eliminate confusion and make your code an open book that’s easy to read and understand. So, keep your variable names clear and your code explicit.
3. The Power of Docstrings and Comments
Think of comments as your code’s personal tour guide. They illuminate the ‘what’, ‘why’, and underlying assumptions of your code. However, remember that well-written code largely speaks for itself, so use comments judiciously.
Docstrings, on the other hand, serve a dual purpose. Not only do they explain the functionality of your functions and classes, but they also play a crucial role in automated documentation. Integrated Development Environments (IDEs) and automated documentation tools use docstrings to provide additional insights about your functions, methods, and classes.
By incorporating detailed docstrings and meaningful comments into your code, you’re essentially embedding the documentation within the code itself. This practice can significantly reduce the need for separate, external documentation.
Moreover, you can leverage various tools to automate the generation of well-structured documentation based on your inline comments and docstrings. This not only ensures that your documentation stays up-to-date with your code, but also frees you from the tedious task of manually maintaining separate documentation.
So, write less, but write smart. Let your code and its embedded documentation tell the story, and let automation handle the rest.
A simple example:
def calculate_bmi(height, weight):
"""
Calculate and return the Body Mass Index (BMI).
:param height: The height in meters.
:param weight: The weight in kilograms.
:return: The calculated BMI.
"""
# Ensure the height is greater than 0 to avoid ZeroDivisionError
if height > 0:
return weight / (height ** 2)
else:
return None
In this code, the function calculate_bmi
is well-documented with a docstring at the beginning, which explains what the function does, its parameters, and its return value. This is the ‘what’.
The inline comment explains why we check if height > 0
, which is to avoid a ZeroDivisionError
. This is the ‘why’.
Remember, comments should not state the obvious, but rather explain complex logic, assumptions, or important decisions related to your code. Good code is self-explanatory, and comments should be used to complement, not replace, good code.
If you would like me to know more about automated documentation generation and how to use docstrings to your advantage in your own IDE's. Let me know in the comments.
4. Build with Modularity in Mind
Modular code is like LEGO blocks—easy to assemble and reuse. It simplifies testing and maintenance, making your codebase resilient and adaptable.
# rectangle.py
def calculate_area(length, width):
return length * width
def calculate_perimeter(length, width):
return 2 * (length + width)
# circle.py
import math
def calculate_area(radius):
return math.pi * radius ** 2
def calculate_perimeter(radius):
return 2 * math.pi * radius
# main.py
import rectangle
import circle
print("Rectangle area:", rectangle.calculate_area(5, 4))
print("Rectangle perimeter:", rectangle.calculate_perimeter(5, 4))
print("Circle area:", circle.calculate_area(3))
print("Circle perimeter:", circle.calculate_perimeter(3))
In this code, rectangle.py
and circle.py
are modules that contain functions related to rectangles and circles, respectively. main.py
imports these modules and uses their functions to calculate areas and perimeters.
This is an example of modular programming. Each module is like a LEGO block that can be easily assembled and reused. It simplifies testing (since each module can be tested independently) and maintenance (since changes in one module don’t affect others). This makes your codebase resilient and adaptable.
5. Automate Testing and Catch Problems Early
Automated tests are your safety net. Tools like Jest for JavaScript or PyTest for Python help catch bugs early and keep your codebase robust.
Suppose we have a simple Flask application with a route that returns a greeting:
# app.py
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/greet', methods=['GET'])
def greet():
return jsonify(message="Hello, World!")
We can write a test for this route that we could then use in a CI (continuous integration) automation pipeline.
# test_app.py
import pytest
from app import app
def test_greet():
with app.test_client() as client:
response = client.get('/greet')
assert response.status_code == 200
assert response.get_json() == {"message": "Hello, World!"}
In this code, test_greet
is a test case for our /greet
route. We’re using Flask’s test client to send a GET request to the route, and then we’re using the assert
statement to verify that the route returns the expected status code and JSON response.
To run the tests we use.
pytest test_app.py
PyTest will automatically discover and run the tests, and report any failures. This allows us to catch and fix bugs early, and ensures that our backend remains robust as we make changes and additions. Automated testing is an absolute requirement for successful backend deployments! If you would like to learn more about automated testing, leave a comment!
6. Master the Art of Version Control
Version control is your time machine. With Git, you can track changes, collaborate without conflicts, and revert when necessary.
Scenario: Imagine a team working on a web application. The team consists of multiple developers, each working on different features of the application.
Step 1: Setting up the Repository The team sets up a repository on GitHub. This repository has a main branch, often called master
or main
.
Step 2: Working on Features When a developer starts working on a new feature, they create a new branch from main
. This branch is often named after the feature or the task (e.g., add-login-button
or fix-navigation-bug
). The developer then works on their feature in this separate branch without affecting the main
branch.
Step 3: Committing Changes As the developer makes progress on their feature, they make commits to their branch. These commits are like save points, which capture the changes made to the code at a particular point in time.
Step 4: Pushing Changes Once the feature is complete, the developer pushes their branch to the GitHub repository. This makes the branch and its commits available to the other members of the team.
Step 5: Pull Requests and Code Reviews The developer then opens a pull request on GitHub. This is a request to merge the changes from their feature branch into the main
branch. Other team members can review the changes, provide feedback, and approve the pull request.
Step 6: Continuous Integration The team can set up continuous integration (CI) with GitHub Actions or other CI tools. When a pull request is opened, the CI tool automatically runs tests on the changes. If the tests pass, the pull request can be merged. If the tests fail, the developer needs to fix the issues before the pull request can be merged.
Step 7: Merging Changes Once the pull request is approved and all tests pass, the changes are merged into the main
branch. This is typically done using the “Squash and Merge” option on GitHub, which combines all the commits into one and merges it into the main
branch.
Step 8: Deploying Changes After the changes are merged, they can be deployed to the production environment. The team can set up continuous deployment (CD) to automatically deploy changes when they are merged into the main
branch.
This workflow allows the team to work together efficiently. Each developer can work on their own features without interfering with others. The use of pull requests and continuous integration ensures that the code is reviewed and tested before it is merged. This helps catch bugs early and ensures that the main
branch always contains stable, deployable code. The use of version control with Git allows the team to track changes, collaborate without conflicts, and revert changes if necessary. It’s a powerful way to manage a software project!
7. Write Code That Tells a Story
Your code should narrate its own purpose. Descriptive function names and clear logic make your code an open book.
Your code should be self-explanatory, making it easy for others (and future you) to understand. Here’s an example:
Consider a piece of code that calculates the distance between two points in a 2D space. Here’s a version without storytelling:
def d(x1, y1, x2, y2):
return ((x2-x1)**2 + (y2-y1)**2)**0.5
While this code is correct and efficient, it’s not immediately clear what it does or what the parameters represent. Will future you even know what this does?
With descriptions written in a story telling mindset.
import math
def calculate_distance(point1, point2):
"""
Calculate and return the Euclidean distance between two points in a 2D space.
:param point1: A tuple representing the x and y coordinates of the first point.
:param point2: A tuple representing the x and y coordinates of the second point.
:return: The Euclidean distance between point1 and point2.
"""
x1, y1 = point1
x2, y2 = point2
dx = x2 - x1
dy = y2 - y1
squared_distance = dx**2 + dy**2
distance = math.sqrt(squared_distance)
return distance
In this version, the function name and parameter names are descriptive, making it clear what the function does and what each parameter represents. The code inside the function is broken down into small, logical steps, each with a clear purpose. This makes the code easier to read and understand, effectively telling a story of how the distance is calculated.
Pro Tip: Remember, your code is more often read than it is written, so clarity should be a priority.
8. Set the Bar with Coding Standards
Coding standards are like a dress code for your code. They ensure consistency, readability, and professionalism in your codebase.
For Python, one of the most widely adopted coding standards is PEP 8. It provides guidelines for code layout, naming conventions, and other aspects of coding in Python. Following these standards can make your code more readable and maintainable.
However, manually checking your code against these standards can be time-consuming. That’s where linters and automated linting come in. A linter is a tool that analyzes your code to detect potential errors, bugs, stylistic errors, and suspicious constructs.
For Python, tools like pylint
or flake8
can be used to check your code against PEP 8 and other standards. These tools can be integrated into your text editor or IDE, providing real-time feedback as you write your code.
To supercharge your workflow, you can set up automated linting using Continuous Integration (CI) tools like GitHub Actions or GitLab CI/CD. With this setup, every time you push your code to the repository, the CI tool will automatically run the linter and report any issues. This ensures that all code in the repository meets your coding standards.
Automated linting not only helps maintain the quality of your codebase, but also saves time on manual code reviews and helps catch issues early in the development process. It’s a powerful tool for any development team.
9. Embrace the Art of Refactoring
Refactoring is your code’s spa day. It’s about improving structure and performance without altering functionality. Sometimes we want to extract a method to simplify functions back to performing a single job. This feature is built into JetBrains IDEs and other tools. Here’s a refactoring snippet:
# Before Refactoring
def calculate_total(cart):
total = 0
discount = 0
for item in cart:
total += item['price']
if item['category'] == 'electronics':
discount += item['price'] * 0.1
return total - discount
# After Refactoring
def calculate_discount(item, discount_rate):
if item['category'] == 'electronics':
return item['price'] * discount_rate
return 0
def calculate_total(cart):
total = 0
discount = 0
for item in cart:
total += item['price']
discount += calculate_discount(item, 0.1)
return total - discount
Pro Tip: VSCode or JetBrains IDE's like Pycharm or IntelliJ have this exact functionality built in. Highlight some code and right click. Let your tools make your life easier and your code cleaner!
10. Seek Continuous Improvement through Feedback
Feedback is the fuel that drives continuous improvement in coding and beyond. It’s like a compass, guiding you towards better practices, cleaner code, and more efficient solutions.
Consider code reviews, a common practice in software development where peers review each other’s code. They’re not just about finding bugs or enforcing coding standards. They’re an opportunity for learning and growth.
When you submit your code for review, you’re inviting fresh perspectives. Your peers might suggest a more efficient algorithm, point out a potential edge case you missed, or help you see how your code fits into the larger project.
On the flip side, when you review others’ code, you expose yourself to new ideas and approaches. You learn from the solutions others have come up with, and you deepen your understanding of the codebase.
In both cases, you’re not just improving the code—you’re improving as a developer. You’re learning to think critically about code, to communicate your ideas clearly, and to collaborate effectively with your peers.
So, embrace feedback. Seek it actively, accept it graciously, and act on it constructively. Remember, every piece of feedback is a stepping stone on your path to becoming a better developer. Keep coding, keep learning, and keep growing!
Conclusion
In the grand journey of coding, remember that clean code isn’t just about writing—it’s about rewriting, refining, and reaching for excellence. It’s about transforming a simple script into a masterpiece of logic and functionality.
So, as you embark on your next project, apply these tips. Watch as they weave magic into your codebase, making it more readable, maintainable, and efficient. And as you witness this transformation, don’t forget to share your journey.
Your experiences, your learnings, your tips, and yes, even your horror stories, are all valuable. They’re lessons learned, stepping stones that have brought you to where you are now. So, share them in the comments below!
Remember, every line of code tells a story, and every story is worth sharing. We all learn from each other, growing together in this wonderful community of coders.
So, keep coding, keep learning, and most importantly, keep sharing. Because every piece of clean code you write is not just an achievement, it’s a step forward for you and for the coding community.
Subscribe to my newsletter
Read articles from Derek Armstrong directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Derek Armstrong
Derek Armstrong
I share my thoughts on software development and systems engineering, along with practical soft skills and friendly advice. My goal is to inspire others, spark ideas, and discover new passions.