Backend / Theory foundations / 04_dry_kiss_principles.md

DRY and KISS Principles in Python

Updated 3 interview angles 4 min read source
On this page6
  1. DRY (Don’t Repeat Yourself)
  2. KISS (Keep It Simple, Stupid)
  3. Common Patterns Combining DRY and KISS
  4. Best Practices
  5. When to Break These Principles
  6. Interview angle

DRY and KISS Principles in Python

DRY (Don’t Repeat Yourself)

The DRY principle states that every piece of knowledge or logic should have a single, unambiguous representation within a system. This reduces code duplication and makes maintenance easier.

Bad Examples (Not DRY)

python
# Example 1: Repeated validation logic
def validate_user(user_data):
    if len(user_data['username']) < 3:
        raise ValueError("Username must be at least 3 characters")
    if not user_data['email'].contains('@'):
        raise ValueError("Invalid email format")
    
def validate_admin(admin_data):
    if len(admin_data['username']) < 3:
        raise ValueError("Username must be at least 3 characters")
    if not admin_data['email'].contains('@'):
        raise ValueError("Invalid email format")

# Example 2: Repeated calculation
def calculate_circle_area(radius):
    return 3.14159 * radius * radius

def calculate_cylinder_volume(radius, height):
    return 3.14159 * radius * radius * height

Good Examples (DRY)

python
# Example 1: Centralized validation logic
def validate_credentials(data):
    if len(data['username']) < 3:
        raise ValueError("Username must be at least 3 characters")
    if not data['email'].contains('@'):
        raise ValueError("Invalid email format")

def validate_user(user_data):
    validate_credentials(user_data)

def validate_admin(admin_data):
    validate_credentials(admin_data)

# Example 2: Using constants and helper functions
import math

def calculate_circle_area(radius):
    return math.pi * radius ** 2

def calculate_cylinder_volume(radius, height):
    return calculate_circle_area(radius) * height

More DRY Examples

python
# Using decorators to avoid repetition
from functools import wraps

def log_execution(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Executing {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Finished {func.__name__}")
        return result
    return wrapper

@log_execution
def process_data():
    pass

@log_execution
def analyze_results():
    pass

# Using inheritance to avoid repetition
class BaseValidator:
    def validate_age(self, age):
        if age < 0 or age > 120:
            raise ValueError("Invalid age")
    
    def validate_email(self, email):
        if '@' not in email:
            raise ValueError("Invalid email")

class UserValidator(BaseValidator):
    def validate(self, user_data):
        self.validate_age(user_data['age'])
        self.validate_email(user_data['email'])

class AdminValidator(BaseValidator):
    def validate(self, admin_data):
        self.validate_age(admin_data['age'])
        self.validate_email(admin_data['email'])

KISS (Keep It Simple, Stupid)

The KISS principle advocates for simplicity in design and implementation. Code should be simple to understand and maintain.

Bad Examples (Not KISS)

python
# Overcomplicated solution
def get_even_numbers(numbers):
    result = []
    for i in range(len(numbers)):
        current_number = numbers[i]
        if current_number % 2 == 0:
            result.append(current_number)
    return sorted(result, key=lambda x: -x)

# Overly complex class structure
class NumberProcessor:
    def __init__(self):
        self.numbers = []
    
    def add_number(self, number):
        self.numbers.append(number)
    
    def get_number_at_index(self, index):
        return self.numbers[index]
    
    def process_numbers(self):
        return sum(self.numbers)

Good Examples (KISS)

python
# Simple and clear solution
def get_even_numbers(numbers):
    return [n for n in numbers if n % 2 == 0]

# Simple function instead of complex class
def process_numbers(numbers):
    return sum(numbers)

More KISS Examples

python
# Complex way (avoid)
def get_user_status(user):
    status = ""
    if user.is_active:
        if user.is_premium:
            if user.subscription_expired:
                status = "Premium (Expired)"
            else:
                status = "Premium"
        else:
            if user.free_trial:
                status = "Trial"
            else:
                status = "Basic"
    else:
        status = "Inactive"
    return status

# Simple way (better)
def get_user_status(user):
    if not user.is_active:
        return "Inactive"
    
    if user.is_premium:
        return "Premium (Expired)" if user.subscription_expired else "Premium"
    
    return "Trial" if user.free_trial else "Basic"

# Complex way (avoid)
class DateFormatter:
    def __init__(self, date):
        self.date = date
        self.formatted = None
        
    def format_date(self):
        self.formatted = self.date.strftime("%Y-%m-%d")
        return self
    
    def get_formatted_date(self):
        if self.formatted is None:
            self.format_date()
        return self.formatted

# Simple way (better)
def format_date(date):
    return date.strftime("%Y-%m-%d")

Common Patterns Combining DRY and KISS

python
# Configuration management
class Config:
    _instance = None
    
    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance
    
    def __init__(self):
        self.settings = {
            'timeout': 30,
            'retries': 3,
            'base_url': 'https://api.example.com'
        }

# Error handling
def handle_exceptions(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except ValueError as e:
            print(f"Validation error: {e}")
        except Exception as e:
            print(f"Unexpected error: {e}")
    return wrapper

@handle_exceptions
def process_user_data(data):
    # Process data here
    pass

Best Practices

  1. For DRY:

    • Create utility functions for common operations
    • Use inheritance for shared behavior
    • Implement decorators for cross-cutting concerns
    • Centralize configuration and constants
  2. For KISS:

    • Write small, focused functions
    • Avoid unnecessary abstractions
    • Use clear, descriptive names
    • Favor readability over cleverness
  3. General:

    • Comment only when necessary
    • Use built-in Python features
    • Follow the standard library’s conventions
    • Write self-documenting code

When to Break These Principles

Sometimes it’s okay to break these principles:

  1. DRY:

    • When duplication is simpler than abstraction
    • When shared code would create tight coupling
    • For temporary or prototype code
  2. KISS:

    • When performance is critical
    • When implementing complex algorithms
    • When following established design patterns

Interview angle 3

  • “When is DRY wrong?” - when the duplication is coincidental rather than semantic. Two pieces of code that look alike but change for different reasons should stay separate; unifying them creates a coupling that fights every future change. The rule is to abstract on the third occurrence, not the second.
  • “What does KISS mean concretely?” - choose the solution a colleague can understand and modify without asking you. Cleverness that saves three lines and costs an hour of comprehension is a net loss.
  • “How does YAGNI interact with extensibility?” - build for today’s requirement with clean boundaries, so tomorrow’s is cheap to add. Speculative generality is the failure it warns against: an abstraction built for a requirement that never arrives is pure cost.