How to Apply Uncle Bob’s Clean Code Principles in Python

How to Apply Uncle Bob’s Clean Code Principles in Python

Uncle Bob, the father of all coders, released the book Clean Code in 2008. If you are serious about software engineering, you must read this book. In this article, we will summarize this book and see how we can apply its principles to our Python code. This is not about the SOLID principles discussed in the book but rather about real situations where you need to follow them in your code base.

The principles Robert Martin discusses are applicable to any programming language, so they are essential for any software developer, in my opinion. Bad code has never helped anyone yet.

Want to see these principles in action? Check out my YouTube video here:

What exactly is clean code?

Clean code, as introduced by Robert C. Martin in his book Clean Code: A Handbook of Agile Software Craftsmanship, is code that is easy to read, understand, and maintain. It emphasizes readability, simplicity, and maintainability. Clean code is well-structured, follows standard conventions, and is free of unnecessary complexity. It is also modular and testable, with functions and methods that have a single responsibility and are designed to be easily modified and extended without introducing bugs. The ultimate goal of clean code is to create a codebase that is robust, flexible, and a pleasure to work with.

Key principles of clean code

These general rules are straightforward but powerful. Let’s delve into the details and see how to implement them in our favorite programming language, Python, with clear examples.

Follow Standard Conventions

Following standard conventions is crucial for writing clean and maintainable code. In Python, this means adhering to PEP 8, the Python Enhancement Proposal that provides guidelines and best practices on how to write Python consistent code.

Keep It Simple, Stupid (KISS)

Simplicity should be a key goal in your design. Simpler code is easier to read, understand, and maintain.

This can be controversial with the SOLID principles of this book. I definitely can't say that applying them is easier than not doing so, but it's true that they can make your life better in the long run.

Boy Scout Rule

The Boy Scout Rule suggests that you should always leave the code cleaner than you found it. This means if you find messy code, clean it up as you make changes.

Always Find the Root Cause

Instead of applying quick fixes, always look for the root cause of the problem. This prevents recurring issues and helps maintain the integrity of the system.


With that said let's see how to apply clean code principles in our Python code.

Prefer polymorphism to if/else

Polymorphism in simple words is the ability of different objects to be treated as instances of the same class through a common interface. It allows the same operation to behave differently on different classes. For example, if different classes have a method with the same name, polymorphism lets you call that method on objects of those classes and get results specific to their class implementation. It enables flexibility and reuse in code.

Polymorphism allows you to avoid complex conditional logic by leveraging inheritance and method overriding. This approach aligns with the Open/Closed Principle.

Bad example with if/else

def get_discount(customer_type): 
    if customer_type == "regular": 
        return 0.1 
    elif customer_type == "premium": 
        return 0.2 else: 
    return 0.0

Good example with polymorphism

class Customer: 
    def get_discount(self): 
        return 0.0

class RegularCustomer(Customer): 
    def get_discount(self): 
        return 0.1

class PremiumCustomer(Customer): 
    def get_discount(self): 
        return 0.2

def get_customer_discount(customer): 
    return customer.get_discount()

Use Dependency Injection

Dependency Injection (DI) is a design pattern that allows for the injection of dependencies into a class, promoting decoupling and enhancing testability and maintainability.

When a class creates its own dependencies internally, it becomes tightly coupled with those dependencies. This means that the class has direct knowledge of and relies on specific implementations of its dependencies, making it harder to change or replace those dependencies without modifying the class itself. This violates the principle of Dependency Inversion, which suggests that high-level modules should not depend on low-level modules, but both should depend on abstractions.

On the other hand, Dependency Injection allows dependencies to be injected into a class from the outside. This means that the class does not need to know how to create its dependencies but instead relies on them being provided externally. This promotes loose coupling between classes, as the class only depends on abstractions (interfaces or abstract classes) rather than concrete implementations. It also makes it easier to test the class, as dependencies can be mocked or replaced with test doubles during testing.

Overall, Dependency Injection leads to code that is more flexible, easier to maintain, and better adheres to the principles of object-oriented design.

No DI

class Service: 
    def init(self): 
        self.repository = Repository()

    def perform_action(self):
        data = self.repository.get_data()

DI

class Service: 
    def init(self, repository): 
        self.repository = repository

    def perform_action(self):
        data = self.repository.get_data()
        # perform action with data

repository = Repository() 
service = Service(repository)

Prevent Over-Configurability and Don't Use Flag Arguments

Keeping software simple means not adding unnecessary settings or options. Flag arguments can make functions complex and harder to understand.
Over-configurability in code can cause problems such as complexity, increased maintenance burden, code smells, and reduced readability. When functions have too many configuration options, it often indicates a violation of the Single Responsibility Principle (SRP), leading to unclear responsibilities and poorer code organization. Additionally, long and complex configurations can make the code harder to read and understand.

Bad example with flag arguments

def create_user(name, email, is_admin=False): 
    user = User(name, email) 
    if is_admin: 
        user.set_admin_permissions() 
    return user

Good example without flag arguments

def create_user(name, email): 
    return User(name, email)

def create_admin_user(name, email): 
    user = User(name, email) 
    user.set_admin_permissions() 
    return user

Follow the Law of Demeter

A class should know only its direct dependencies. This promotes loose coupling and encapsulation, making the code more modular and easier to maintain.

Bad example violating the Law of Demeter

def get_user_info(user): 
    address = user.get_address() 
    city = address.get_city() 
    return city

Good example following the Law of Demeter

def get_user_info(user): 
    return user.get_city()

Avoid Logical Dependency

Methods within a class should not rely on the internal state or behavior of other methods within the same class. Each method should be self-contained and independent.

Bad example with logical dependency

class Calculator: 

    def init(self): 
        self.result = 0

    def add(self, number):
        self.result += number

    def subtract(self, number):
        self.result -= number

    def get_result(self):
        return self.result

Good example without logical dependency

class Calculator: 

    def add(self, a, b): 
        return a + b

    def subtract(self, a, b):
        return a - b

Avoid Side Effects

Functions without side effects are predictable, easier to test, more modular, safer for parallel execution, and generally lead to more maintainable and readable code. They should only depend on their inputs and produce an output without modifying external state.

This is also the example of “single responsibility principle” from SOLID because function should only be doing one thing. That means that it cannot produce any side effects. If function is summing up 2 variables then it should not log something into console. If function is creating a user in the database it shouldn’t perform validation.

Bad example with side effects

def add_to_list(item, item_list=[]): 
    item_list.append(item) 
    return item_list

Good example with no side effects

def add_to_list(item, item_list=None): 
    if item_list is None: 
        item_list = [] 

    new_list = item_list + [item] 
    return new_list

Is clean code worth reading?

Absolutely. Clean Code by Robert C. Martin is a must-read for anyone serious about software development. It offers practical advice on writing readable, maintainable, and efficient code, which leads to better software quality and easier maintenance. Investing time in this book will significantly improve your coding skills and professional practices.

Conclusion

In summary, we’ve touched upon several principles from Uncle Bob’s Clean Code and shown how to apply them in Python. For a deeper understanding and more comprehensive coverage, I highly recommend reading the book. Let's strive to keep our code quality strong for each other. Check out the GitHub page for the summary of clean code book. Happy coding!