Writing code is always fun. Every software developer in love with their job has no problem discussing, at (very) long length, any tiny feature that’s been recently deployed. They can speak passionately for hours about the details of a certain technical solution they applied to their project.
But, with this love – comes the toll of perfectionism. Inevitably, developers will find themselves in situations with buggy code and can work for hours, without finding the reason their code isn’t compiling or running correctly. Likely hidden behind all the time spent searching for a solution – is either poorly written code, code that is fragile, hard to read or downright confusing.
Developing quality code is a challenge – one driven by short deadlines, vague requirements and pressure. All these factors are what leads to poorly written code and the mounting of technical debt.
As a Python developer, looking back on my software development days, I can remember projects where software quality wasn’t exactly high priority. I worked in situations where fast delivery was engrained in company culture, leaving me no time to refactor code and review code quality. So, after few months of intensive work, my team and I stopped delivering new functionalities. Our work was focused on fixing existing bugs and creating new ones. It got to the point where the codebases started to look like a giant spaghetti monster. Our release plan looked not so different than the image below:
Clean Code – A discovery I should’ve made much earlier.
After spending time reading up on all I could on code quality for my Python projects, I discovered the concept of clean code, a set of values and principles that set the bar on how good code should look & operate.
- Code should be elegant and pleasing to read.
- No duplication should be allowed.
- Code should be covered with tests.
- Every function should do one thing and do it well.
- Codebase should contain only code that is needed.
Following this simple set of principles helped me further develop robust, easy to modify software products.
Talk is cheap. Show me the Code.
1. Naming conventions
It’s pointless to talk about clean code without real examples. So, let’s dive into the principles that can help us achieve clean code with Python. When choosing the right names for variables, functions and methods, developers should seek to answer questions like:
Why am I creating them? What will they be used for? What purpose will they serve?
Below, we have code sample that prints the names of 5 cities taken from a tuple. What’s wrong with this code?
If you take closer look at it, you can see that the code intentions are hidden and it’s difficult to understand what for l variable stands for. We can vastly improve this code readability by using proper variable names.
Now, accessing later cities listed in this code, we can guess that we’re dealing with a collection of cities without having to guess what’s inside the tuple.
Let’s go a little further and look at a class responsible for delivering email to users.
While, this example of code does work well, it is hard to understand the intent of the handle function. Choosing the right function name will improve the code readability.
2. Avoid duplicate code at all costs
Another principle of clean code says that shouldn’t duplicate code. Breaking this rules can lead to:
- Code that is hard to maintain or change
- Errors in code (We might forget to propagate changes across all the code)
To avoid code duplication, we can use Python decorators or context managers to deal with duplications and separate business logic from implementation details.
In the code snippet above, we have functions that are responsible for delivering email or sms messages to users as well as logging errors in case of network issues, invalid phone number or email.
We can see that we have duplicated the code for logging failures and success cases. To eliminate the duplicate code, we can refactor it using decorators.
Context managers can also help you reduce the amount of duplicated code when dealing with allocation and releasing resources (i.e. reading file, database connection). You can think of these as elegant solutions for separating business logic and accessing resources.
Python syntax allows you to define your own context managers and use them while accessing and releasing resources. For example, in the code below, you can safely run backup service by first stopping the database service and starting it again later on.
Make your code SOLID
SOLID is a set of principles, defined by Robert C. Martin in the early 2000’s and an acronym that stands for:
- Single Responsibility Principle
- Open and Closed Principle
- Lisvok Sub situation Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Following those will make the management of dependencies in your code easier to handle and your codebase easier to understand and extend. Doesn’t that sound great? Now let’s go through all these principles using Python.
1. Single Responsibility Principle (SRP)
A class should have only one responsibility and only one reason to change. That means a class should not perform multiple jobs. Take a look at the code below.
The class use has violated the RSP principle because it’s mixed-up implementation details with business logic. Also, it has logic related to database connection and user representation.
In order to follow the Single Responsibility principle, we can separate the User class in two classes. The first will handle database connection and the second will represent user entity in the business logic of the application.
2. Open / Closed principle (OCP)
Software entities (classes, function, module) are open for extension, but not for modification (or closed for modification). Developers often need to extend third party libraries (ex.: Django Models), but should keep the default behavior of the class we inherit from. The example below shows the implementation of the open/closed principle. Because we can’t edit or modify the project source code of Django, we can make use of OCP to extend the Django Model class and add extra logic before saving any changes to the database.
3. Lisvok Substitution Principle (LSP)
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T. In simple words, this means that every subclass that inherits from parent class, can be replaced with superclass without breaking program execution.
Class HttpConnection violates LSP, since the contract for its parent class wasn’t specified in the secured parameter. We can improve the code by adding “kwargs” parameter to HttpConnection class. This way, all subclasses of the Connection class will have the same contract for the connect method.
4. Interface Segregation Principle (ISP)
A client should not be forced to implement an interface that it does not use.
In the code example with classes, “HTTPConnection” and “SSHConnection” we can see an ISP violation, since those classes should implement only methods they need – not all provided by parent. Connection class and small code refactoring can ensure ISP implementation.
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
In the example above, we can see a DIP violation since “ConnectionService” class depends on “udp connector”. Directly plugged into the initialization method, we can’t extend it with “tcp connection” or another type of connector since we directly tied it to the udp one. Below, we can find an example with an implementation of DIP, where “ConnectionService” isn’t directly dependent on a certain type of connector.
Tools for Clean Code with Python
Achieving quality software can also be done by using tools for code analysis such as:
These can be integrated in the CI/CD process and help developers follow clean code principles.
Writing clean code requires discipline and understating of a certain set of rules like keeping code readable and avoiding duplicated code at all costs. Using Python syntax, choosing relevant naming conventions and following SOLID principles can help software developers achieve clean code and write robust, flexible software.
How about you? What are the best practices that have made your developer life easier? Feel free to share your thoughts in the Comments section below.