Kushal C

.

homeblog

Writing cleaner code

Cover Image for Writing cleaner code
Kushal Cuttari
Kushal Cuttari

This is a collection of axioms to writing cleaner code that I've personally found to be useful along with some examples of how/where they can be applied.

Axioms to writing cleaner code


1. Avoid nesting code

Consider the following example:

def process_user_data(users):
    premium_users = []
    base_users = []
    if users:
      for user in users:
        if user.is_active and user.age >= 18:
          if user.has_subscription and user.subscription.is_valid() and user.subscription.type == "premium":                 
            premium_users.append(user)
          else:
            base_users.append(user)
    if len(premium_users) > 0 or len(base_users) > 0:
      return {
        "status": "success",
        "premium_users": premium_users,
        "base_users": base_users
    }
    else:
      return {"status": "error", "message": "No base or premium users found"}

Now consider the following revised example:

def is_premium_eligible(user):
    return (
        user.is_active 
        and user.age >= 18
        and user.has_subscription 
        and user.subscription.is_valid() 
        and user.subscription.type == "premium"
    )

def process_user_data(users):
    if not users:
        return {"status": "error", "message": "No base or premium users found"}
        
    premium_users = []
    base_users = []
    
    for user in users:
        if not (user.is_active and user.age >= 18):
          continue
            
        if is_premium_eligible(user):
            premium_users.append(user)
        else:
            base_users.append(user)
            
    if not (premium_users or base_users):
        return {"status": "error", "message": "No base or premium users found"}
        
    return {
        "status": "success",
        "premium_users": premium_users,
        "base_users": base_users
    }

Which one is easier to read and reason about? Its clearly the second example, but the question becomes why?

The answer is that we've avoided nesting code. Every level of indentation is another layer of context that you have to keep in your head. The more nested your code is, the more context you need to reason about the code you've written.

Consider the first example, we need to keep track of multiple conditions at every level of indentation. At the first level we need to ensure that the users object is not empty, at the second level we need to ensure that the user is active and at least 18 years old, at the third level we need to check if the user has a valid subscription and that subscription is for a premium plan.

Now consider the second example, we only need to keep track of two conditions at any level of indentation.

  • We're looking for premium users
  • We're looking for base users

We return early if we don't have any users or if we don't find any premium users and that makes subsequent logic much easier to reason about as we limit the number of conditions we need to keep track of.

One easy way to avoid nesting is thinking about logical invariants. If you find yourself writing code that is doing something like if exists, then loop through and do something. You can often replace this with if not exists then return early, otherwise loop through and do something.

Specifically in the example above:

def process_user_data(users):
    if not users:
        return {"status": "error", "message": "No eligible premium users found"}
    ...
    for user in users:
        if not (user.is_active and user.age >= 18):
          continue
    ...
    if not (premium_users or base_users):
        return {"status": "error", "message": "No base or premium users found"}

Every single condition that needs to be met is at the top of the function, now we know that we don't have to worry about those conditions in the subsequent logic.


2. Avoid optional parameters whenever possible

This is a bit of a subjective axiom, but I've found that optional parameters can make code harder to maintain if not used judiciously. Consider the following example:

def create_user(name, email, role="user", active=True, login_attempts=0):
    return {
        "name": name,
        "email": email,
        "role": role,
        "active": active,
        "login_attempts": login_attempts
    }

# Usage
user1 = create_user("Alice", "alice@example.com")  # What are the defaults?
user2 = create_user("Bob", "bob@example.com", active=False)  # Skipping 'role'

Optional parameters create implicit dependencies that aren't visible at the call site. When reading code, developers need to look up the function definition to understand the default behavior. Furthermore, if you have a function with a lot of optional parameters, it becomes hard to tell what the function is actually doing just by looking at the call site. Currently we have a function with 3 optional parameters, what happens when we add more? To appropriately test the function we now have to create a new test case for every combination of optional parameters. In our exmaple above there are 2^3 = 8 combinations. However imagine if we had 10 optional parameters, now we need to create 2^10 = 1024 test cases. This is a combinatorial explosion and quickly makes our test suite much harder to maintain.

Instead of using optional parameters, we can use configuration objects. e.g

@dataclass
class UserConfig:
    name: str
    email: str
    role: str = "user"
    active: bool = True
    login_attempts: int = 0

    def validate(self):
      # insert validation logic here
      ...

This approach:

  • Makes dependencies explicit through a dedicated configuration object
  • Enables validation of parameter combinations
  • Keeps the function signature clean and simple
  • Makes it easier to add new options without breaking existing code
  • Allows for reuse of common configurations

3. Write self documenting code

This axiom is closely related to the first example, you want to avoid writing code that is hard to read and reason about. Appropriate variable names and function names can go a long way in making your code more readable and lower the cognitive load for a developer reading your code.

# Instead of:
def c(s): 
    t = 0  
    for n in s: 
        if n % 2 == 0: 
            t += n
    return t

# Write:
def sum_even_numbers(numbers):
    total = 0
    for number in numbers:
        if number % 2 == 0:
            total += number
    return total

Just by introducing the variable names we can make the code go from hard to read to easy to understand.


4. Keep functions small (and preferably stateless)

Small, stateless functions are like LEGO blocks for writing better code. Instead of creating large, complex functions that try to do many things at once, we break them down into small, focused pieces that each do just one thing well. These small functions take inputs and return outputs without changing anything else in the program - just like a calculator that gives you 4 when you input 2+2, every single time. This makes code easier to write, test, fix, and change later. When you need to solve bigger problems, you can simply snap these small functions together, just like building something complex out of simple LEGO pieces.

Consider the following example:

# Original complex function with state
class OrderProcessor:
    def __init__(self):
        self.tax_rates = {"CA": 0.0725, "NY": 0.045}
        self.shipping_rates = {"standard": 5.99, "express": 15.99}
        self.discount_rules = {"SUMMER": 0.1, "WELCOME": 0.2}
        
    def process_order(self, items, state, shipping_type, coupon=None):
        # Complex function doing many things
        total = 0
        for item in items:
            if item.get("clearance"):
                total += item["price"] * 0.7
            else:
                total += item["price"]
        
        # Apply coupon if valid
        if coupon and coupon in self.discount_rules:
            total = total * (1 - self.discount_rules[coupon])
        
        # Add tax
        if state in self.tax_rates:
            total += total * self.tax_rates[state]
        
        # Add shipping
        if shipping_type in self.shipping_rates:
            total += self.shipping_rates[shipping_type]
            
        return round(total, 2)

We have a class that is responsible for processing orders, it has a lot of internal state and a lot of logic to determine the total price of an order. Its responsible for tax, shipping, discounts and more. This is a lot of responsibility for a single class, but this introduces a problem. What happens when we want to add a new discount rule? We now have to change the class. What happens when we want to add a new tax rate? We now have to change the class. We've introduced a lot of rigidity into our code.

Now consider the following refactored version:


@dataclass
class Item:
    price: Decimal
    clearance: bool = False

@dataclass
class PricingRules:
    tax_rates: dict[str, Decimal]
    shipping_rates: dict[str, Decimal]
    discount_rules: dict[str, Decimal]

def calculate_item_price(item: Item) -> Decimal:
    """Calculate single item price considering clearance status."""
    if item.clearance:
        return item.price * Decimal("0.7")
    return item.price

def calculate_subtotal(items: List[Item]) -> Decimal:
    """Sum up all item prices."""
    return sum(calculate_item_price(item) for item in items)

def apply_discount(amount: Decimal, rules: PricingRules, coupon: Optional[str]) -> Decimal:
    """Apply discount if coupon is valid."""
    if coupon and coupon in rules.discount_rules:
        return amount * (1 - rules.discount_rules[coupon])
    return amount

def calculate_tax(amount: Decimal, state: str, rules: PricingRules) -> Decimal:
    """Calculate tax for given state."""
    tax_rate = rules.tax_rates.get(state, Decimal("0"))
    return amount * tax_rate

def get_shipping_cost(shipping_type: str, rules: PricingRules) -> Decimal:
    """Get shipping cost for specified type."""
    return rules.shipping_rates.get(shipping_type, Decimal("0"))

def calculate_total(
    items: List[Item],
    state: str,
    shipping_type: str,
    rules: PricingRules,
    coupon: Optional[str] = None
) -> Decimal:
    """Calculate final order total by composing small functions."""
    subtotal = calculate_subtotal(items)
    discounted = apply_discount(subtotal, coupon, rules)
    tax = calculate_tax(discounted, state, rules)
    shipping = get_shipping_cost(shipping_type, rules)
    
    return round(discounted + tax + shipping, 2)

This seems a lot more verbose, but the benefit is that we've broken down a complex problem into smaller problems that are each easier to understand and reason about. We've also gained a couple of key benefits:

  • Each function is now easier to test in isolation
def test_calculate_item_price():
    item = Item(price=Decimal("100"), clearance=True)
    assert calculate_item_price(item) == Decimal("70")
  • We can reuse the functions in different parts of our application e.g

If we want to calculate the tax for an order we can simply do the following:

tax_estimate = calculate_tax(amount=Decimal("100"), state="CA", rules=pricing_rules)
  • The code is now more maintainable, if we want to change how we calculate the price of an item we only have to change one function.
# Easy to modify just tax calculation without affecting other logic
def calculate_tax(amount: Decimal, state: str, rules: PricingRules) -> Decimal:
    tax_rate = rules.tax_rates.get(state, Decimal("0"))
    # New: Add local city tax if applicable
    city_tax = get_city_tax(state)
    return amount * (tax_rate + city_tax)

Now we can modify the tax calculation without affecting other logic.


This is an ever growing list of axioms, I'll keep updating it as I land on new principles. Hopefully you found this useful and can leverage these principles in your own codebase.