Back to Posts
An illustration of tangled wires forming a knot inside a computer monitor, emitting steam and question marks, symbolizing the complexity and confusion associated with poor programming practices and the need for best practices for eliminating Python code smells.

Fix Python Code Smells with These Best Practices

By Alyce Osbourne

Are your projects full of pongy Python code? Are you struggling to understand and maintain your codebase? Then, hopefully, this blog post will help you!

1. The “god object” smell

class OnlineStore:
    def search_product(self, query: Query):
        # Logic to search for products in some database
        pass
    def process_order(self, order: Order):
        # Logic to process the order and send confirmation email
        pass
    def handle_payment(self, payment_info: PaymentInfo):
        # Logic to handle payment and update the order status
        pass
    def manage_inventory(self, product_id: int, quantity: int):
        # Logic to manage inventory and update the database
        pass
    # Many more methods

“God objects” are of monolithic design, tackling way too many tasks and responsibilities, violating the Single Responsibility Principle (SRP) of SOLID design. The OnlineStore class in our code sample is responsible for inventory management, order processing, payment acceptance, and product search. Combining all of these duties into a single class may limit our flexibility to introduce new features while increasing testing and maintenance complexity. However, we may enhance this by rewriting the OnlineStore class into more manageable, specialized classes like ProductSearchOrderProcessorPaymentGateway, and InventoryManager. This allows each class to focus on a specific task while adhering to the SRP, making our system easier to comprehend and react to changes.

class ProductSearch:
    def search_product(self, query: Query):
        # Logic to search for products in some database
        pass

class OrderProcessor:
    def process_order(self, order: Order):
        # Logic to process the order and send confirmation email
        pass

class PaymentGateway:
    def handle_payment(self, total: int, payment_info: Payment):
        # Logic to handle payment and update the order status
        pass

class InventoryManager:
    def manage_inventory(self, product_id: int, quantity: int):
        # Logic to manage inventory and update the database
        pass

2. The “duplicate code” smell

class ReportGenerator:
    def generate_sales_report(self, sales_data: list[Report):
        # Preprocessing steps, such as formatting the data into a table.
        # Generate sales report
        pass

    def generate_inventory_report(self, inventory_data: list[Report]):
        # Preprocessing steps (duplicated)
        # Generate inventory report
        pass

When the same code blocks appear more than once, it’s considered duplicate code. In addition to growing the codebase, this redundancy raises the possibility of mistakes and inconsistencies. You must ensure that all duplicates are updated when you make changes to your code. The identical preparation operations are carried out by numerous methods of the ReportGenerator class. We can combine these repetitive procedures into a single preprocess_data method to solve this problem. In this manner, we remove redundancy and align it with the DRY (Don’t Repeat Yourself) coding philosophy. By creating a single preprocessing method, we ensure consistency and ease of implementation for future updates.

class ReportGenerator:
    def preprocess_data(self, data: list[Report]):
        # Common preprocessing steps
        pass

    def generate_sales_report(self, sales_data: list[Report]):
        self.preprocess_data(sales_data)
        # Generate sales report
        pass

    def generate_inventory_report(self, inventory_data: list[Report]):
        self.preprocess_data(inventory_data)
        # Generate inventory report
        pass

3. The “long method” smell

def handle_customer_request(request: CustomerRequest):
    # Validate request
    # Log request details
    # Check inventory
    # Calculate pricing
    # Apply discounts
    # Finalize response
    pass

A “long method” contains too many lines of code and is often challenging to read, understand, and test. From validation to logging to inventory verification to pricing to discounting to response finalization, the handle_customer_request function handles it all in one function. Readability and reusability can be improved by breaking this method down into smaller, more focused functions. By separating the method into smaller, more focused functions, we can improve readability and reusability, as well as simplify unit testing. We should aim to make each method responsible for a singular task.

def handle_customer_request(request: CustomerRequest):
    validate_request(request)
    log_request(request)
    check_inventory(request)
    pricing = calculate_pricing(request)
    apply_discounts(pricing)
    return finalize_response(pricing)

def validate_request(request: Request): pass
def log_request(request: Request): pass
def check_inventory(request: Request): pass
def calculate_pricing(request: Request): pass
def apply_discounts(pricing: int): pass
def finalize_response(pricing: int): pass

4. The “magic numbers” smell

def calculate_shipping_cost(distance: float) -> float:
    return distance * 1.25  # What does 1.25 signify?

“Magic numbers” are those tricky numerical literals that often show up in programming code with no apparent explanation, making the code harder to follow and handle. The calculate_shipping_cost function utilizes the number 1.25 without any context, leaving us to wonder about its purpose and meaning. Instead, we can introduce a constant called PER_MILE_SHIPPING_RATE, which makes it clear that 1.25 represents the cost per mile for shipping. This simple change makes our code much easier to understand and also simplifies changes to this value in the future.

PER_MILE_SHIPPING_RATE = 1.25

def calculate_shipping_cost(distance: float) -> float:
    return distance * PER_MILE_SHIPPING_RATE

5. The “nested conditionals” smell

def approve_loan(application: LoanApplication) -> bool:
    if application.credit_score > 600:
        if application.income > 30000:
            if application.debt_to_income_ratio < 0.4:
                return True
            else:
                return False
        else:
            return False
    else:
        return False

Nested conditional statements can make it difficult to understand the flow of your function. The approve_loan method is surrounded by a series of nested if statements that can be difficult to understand. By refactoring our code so each condition is checked in sequence, we can create a structure that is flatter and much easier to read and comprehend. If you have complex logic mixed in with the conditions, it might be worth abstracting the logic into a separate function to make the conditions easier to read. And if you have a series of conditions that need to be met, consider using the any and all built-in functions to make the conditions more readable.

def approve_loan(application: LoanApplication) -> bool:
    if application.credit_score <= 600:
        return False
    if application.income <= 30000:
        return False
    if application.debt_to_income_ratio >= 0.4:
        return False
    return True

Using any/all built-in functions:

def approve_loan(application: LoanApplication) -> bool:
    return all([
        application.credit_score > 600,
        application.income > 30000,
        application.debt_to_income_ratio < 0.4
    ])

Final thoughts

By finding and refactoring this stinky code, we can make our codebase more maintainable, readable, and scalable. This will make it easier for us to add new features, fix bugs, and collaborate with other developers. For more pongy code problems and their solutions, check out my videos on other code smells here.

Improve your code with my 3-part code diagnosis framework

Watch my free 30 minutes code diagnosis workshop on how to quickly detect problems in your code and review your code more effectively.

When you sign up, you'll get an email from me regularly with additional free content. You can unsubscribe at any time.

Recent posts