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 ProductSearch
, OrderProcessor
, PaymentGateway
, 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.