Sign In Create Account
Clean Code

Clean Code Principles

Apply SOLID principles and clean code practices for maintainable, testable Python applications.

SOLID Principles in Python

Follow SOLID principles to create flexible, maintainable, and testable code.

Single Responsibility Principle (SRP)

Each class should have only one reason to change.

Single Responsibility Example

# ❌ Violates SRP - multiple responsibilities
class UserAccount:
    def __init__(self, username: str, email: str):
        self.username = username
        self.email = email
    
    def save_to_database(self):
        """Database operation - different responsibility"""
        pass
    
    def send_welcome_email(self):
        """Email operation - different responsibility"""
        pass
    
    def validate_email(self) -> bool:
        """Validation logic - different responsibility"""
        pass

# ✅ Follows SRP - separated responsibilities
class User:
    """Represents a user entity."""
    
    def __init__(self, username: str, email: str):
        self.username = username
        self.email = email

class UserRepository:
    """Handles user data persistence."""
    
    def save(self, user: User) -> bool:
        """Save user to database."""
        # Database logic
        pass
    
    def find_by_username(self, username: str) -> User | None:
        """Find user by username."""
        # Query logic
        pass

class EmailService:
    """Handles email operations."""
    
    def send_welcome_email(self, user: User) -> bool:
        """Send welcome email to user."""
        # Email logic
        pass

class UserValidator:
    """Validates user data."""
    
    @staticmethod
    def validate_email(email: str) -> bool:
        """Validate email format."""
        import re
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$'
        return re.match(pattern, email) is not None

Open/Closed Principle (OCP)

Classes should be open for extension but closed for modification.

Open/Closed Example

from abc import ABC, abstractmethod

# ✅ Follows OCP - extensible without modification
class PaymentProcessor(ABC):
    """Abstract base for payment processing."""
    
    @abstractmethod
    def process_payment(self, amount: float, currency: str) -> dict:
        """Process payment and return result."""
        pass

class CreditCardProcessor(PaymentProcessor):
    """Process credit card payments."""
    
    def process_payment(self, amount: float, currency: str) -> dict:
        """Process credit card payment."""
        return {
            'success': True,
            'method': 'credit_card',
            'amount': amount,
            'currency': currency,
            'transaction_id': self._generate_transaction_id()
        }
    
    def _generate_transaction_id(self) -> str:
        """Generate unique transaction ID."""
        import uuid
        return str(uuid.uuid4())

class PayPalProcessor(PaymentProcessor):
    """Process PayPal payments."""
    
    def process_payment(self, amount: float, currency: str) -> dict:
        """Process PayPal payment."""
        return {
            'success': True,
            'method': 'paypal',
            'amount': amount,
            'currency': currency,
            'paypal_id': self._get_paypal_transaction_id()
        }
    
    def _get_paypal_transaction_id(self) -> str:
        """Get PayPal transaction ID."""
        # PayPal API logic
        return "PP_12345"

class PaymentService:
    """Service for handling payments - closed for modification."""
    
    def __init__(self):
        self.processors: dict[str, PaymentProcessor] = {}
    
    def register_processor(self, method: str, processor: PaymentProcessor):
        """Register new payment processor."""
        self.processors[method] = processor
    
    def process_payment(self, method: str, amount: float, currency: str) -> dict:
        """Process payment using specified method."""
        if method not in self.processors:
            raise ValueError(f"Unsupported payment method: {method}")
        
        return self.processors[method].process_payment(amount, currency)

# Usage - adding new payment method without modifying existing code
class ApplePayProcessor(PaymentProcessor):
    """Process Apple Pay payments."""
    
    def process_payment(self, amount: float, currency: str) -> dict:
        return {
            'success': True,
            'method': 'apple_pay',
            'amount': amount,
            'currency': currency,
            'touch_id_verified': True
        }

# Register processors
payment_service = PaymentService()
payment_service.register_processor('credit_card', CreditCardProcessor())
payment_service.register_processor('paypal', PayPalProcessor())
payment_service.register_processor('apple_pay', ApplePayProcessor())  # Extension without modification

Dependency Inversion Principle (DIP)

Depend on abstractions, not concretions.

Dependency Inversion Example

from abc import ABC, abstractmethod
from typing import Protocol

# ✅ Follows DIP - depend on abstractions
class NotificationSender(Protocol):
    """Protocol for notification sending."""
    
    def send(self, recipient: str, message: str) -> bool:
        """Send notification to recipient."""
        ...

class EmailSender:
    """Concrete email sender."""
    
    def send(self, recipient: str, message: str) -> bool:
        """Send email notification."""
        print(f"Sending email to {recipient}: {message}")
        return True

class SMSSender:
    """Concrete SMS sender."""
    
    def send(self, recipient: str, message: str) -> bool:
        """Send SMS notification."""
        print(f"Sending SMS to {recipient}: {message}")
        return True

class PushNotificationSender:
    """Concrete push notification sender."""
    
    def send(self, recipient: str, message: str) -> bool:
        """Send push notification."""
        print(f"Sending push notification to {recipient}: {message}")
        return True

class UserNotificationService:
    """Service that depends on abstraction, not concrete implementations."""
    
    def __init__(self, sender: NotificationSender):
        self.sender = sender  # Depends on abstraction
    
    def notify_user_registration(self, user_email: str, username: str):
        """Notify user about successful registration."""
        message = f"Welcome {username}! Your account has been created successfully."
        return self.sender.send(user_email, message)
    
    def notify_password_reset(self, user_email: str):
        """Notify user about password reset."""
        message = "Your password has been reset. Please check your email for instructions."
        return self.sender.send(user_email, message)

# Usage - can easily switch notification methods
email_service = UserNotificationService(EmailSender())
sms_service = UserNotificationService(SMSSender())
push_service = UserNotificationService(PushNotificationSender())

# The service doesn't need to change when switching notification methods
email_service.notify_user_registration("[email protected]", "john_doe")
sms_service.notify_password_reset("1234567890")

SOLID Benefits

  • Maintainability: Easier to modify and extend code
  • Testability: Classes with single responsibilities are easier to test
  • Flexibility: New features can be added without breaking existing code
  • Reusability: Well-designed components can be reused in different contexts

Pythonic Code Patterns

Write idiomatic Python code that leverages language features effectively.

Pythonic Patterns

# ✅ Use list comprehensions appropriately
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Good - simple transformation
squared = [n**2 for n in numbers]
evens = [n for n in numbers if n % 2 == 0]

# Good - but prefer generator for large datasets
even_squares = (n**2 for n in numbers if n % 2 == 0)

# ✅ Use context managers for resource management
class DatabaseConnection:
    """Database connection with context manager support."""
    
    def __init__(self, connection_string: str):
        self.connection_string = connection_string
        self.connection = None
    
    def __enter__(self):
        """Enter context manager."""
        self.connection = self._connect()
        return self.connection
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Exit context manager."""
        if self.connection:
            self.connection.close()
        return False  # Don't suppress exceptions
    
    def _connect(self):
        """Establish database connection."""
        # Connection logic
        pass

# Usage
with DatabaseConnection("postgresql://...") as conn:
    # Connection automatically managed
    result = conn.execute("SELECT * FROM users")

# ✅ Use dataclasses for data containers
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class User:
    """User data class with automatic __init__, __repr__, etc."""
    username: str
    email: str
    created_at: datetime = field(default_factory=datetime.now)
    tags: list[str] = field(default_factory=list)
    is_active: bool = True
    
    def __post_init__(self):
        """Validate data after initialization."""
        if "@" not in self.email:
            raise ValueError(f"Invalid email: {self.email}")

# ✅ Use enumerate instead of range(len())
items = ['apple', 'banana', 'orange']

# Good
for index, item in enumerate(items):
    print(f"{index}: {item}")

# ✅ Use zip for parallel iteration
names = ['Alice', 'Bob', 'Charlie']
ages = [25, 30, 35]
cities = ['New York', 'London', 'Tokyo']

for name, age, city in zip(names, ages, cities):
    print(f"{name}, {age}, lives in {city}")

# ✅ Use * for unpacking
def greet_user(first_name: str, last_name: str, age: int):
    """Greet user with their information."""
    return f"Hello {first_name} {last_name}, you are {age} years old!"

user_data = ('John', 'Doe', 30)
greeting = greet_user(*user_data)

# ✅ Use ** for keyword argument unpacking
user_info = {
    'first_name': 'Jane',
    'last_name': 'Smith', 
    'age': 28
}
greeting = greet_user(**user_info)

Function Design Best Practices

Function Best Practices

from typing import Any
from functools import wraps
import logging

logger = logging.getLogger(__name__)

# ✅ Functions should do one thing well
def calculate_tax(income: float, tax_rate: float) -> float:
    """Calculate tax amount for given income and rate."""
    if income < 0:
        raise ValueError("Income cannot be negative")
    if not 0 <= tax_rate <= 1:
        raise ValueError("Tax rate must be between 0 and 1")
    
    return income * tax_rate

def format_currency(amount: float, currency: str = "USD") -> str:
    """Format amount as currency string."""
    return f"{amount:.2f} {currency}"

# ✅ Use pure functions when possible (no side effects)
def calculate_compound_interest(
    principal: float, 
    rate: float, 
    time: int, 
    compound_frequency: int = 12
) -> float:
    """Calculate compound interest (pure function)."""
    return principal * (1 + rate / compound_frequency) ** (compound_frequency * time)

# ✅ Use default arguments wisely
def create_user_profile(
    username: str,
    email: str,
    *,  # Force keyword-only arguments
    role: str = "user",
    is_active: bool = True,
    preferences: dict[str, Any] | None = None,
    tags: list[str] | None = None
) -> dict[str, Any]:
    """Create user profile with sensible defaults."""
    if preferences is None:
        preferences = {"theme": "light", "notifications": True}
    
    if tags is None:
        tags = []
    
    return {
        "username": username,
        "email": email,
        "role": role,
        "is_active": is_active,
        "preferences": preferences.copy(),  # Defensive copy
        "tags": tags.copy(),  # Defensive copy
        "created_at": datetime.now()
    }

# ✅ Use decorators for cross-cutting concerns
def validate_positive_number(func):
    """Decorator to validate that all numeric arguments are positive."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Validate all numeric arguments
        for arg in args:
            if isinstance(arg, (int, float)) and arg < 0:
                raise ValueError(f"Negative number not allowed: {arg}")
        
        for key, value in kwargs.items():
            if isinstance(value, (int, float)) and value < 0:
                raise ValueError(f"Negative number not allowed for {key}: {value}")
        
        return func(*args, **kwargs)
    return wrapper

@validate_positive_number
def calculate_area_circle(radius: float) -> float:
    """Calculate circle area with automatic validation."""
    return 3.14159 * radius ** 2

# ✅ Use type-safe factory functions
def create_database_config(
    host: str,
    database: str,
    *,
    port: int = 5432,
    username: str = "postgres",
    password: str | None = None,
    ssl_enabled: bool = True
) -> dict[str, Any]:
    """Create database configuration with validation."""
    if not host:
        raise ValueError("Host cannot be empty")
    if not database:
        raise ValueError("Database name cannot be empty")
    if not 1 <= port <= 65535:
        raise ValueError(f"Invalid port number: {port}")
    
    config = {
        "host": host,
        "port": port,
        "database": database,
        "username": username,
        "ssl_enabled": ssl_enabled
    }
    
    if password:
        config["password"] = password
    
    return config

Function Design Guidelines

  • Single Purpose: Each function should do one thing well
  • Pure When Possible: Prefer functions without side effects
  • Meaningful Names: Function names should clearly indicate purpose
  • Reasonable Length: Keep functions focused and concise

Testing and Code Quality

Testing Best Practices

import pytest
from unittest.mock import Mock, patch
# ✅ Write testable code with dependency injection
class UserService:
    """Service for user operations."""
    
    def __init__(self, user_repository, email_service):
        self.user_repository = user_repository
        self.email_service = email_service
    
    def create_user(self, username: str, email: str) -> dict:
        """Create new user account."""
        # Check if user exists
        if self.user_repository.find_by_username(username):
            raise ValueError(f"Username already exists: {username}")
        
        # Create user
        user = {
            "username": username,
            "email": email,
            "id": self._generate_user_id(),
            "created_at": datetime.now()
        }
        
        # Save user
        self.user_repository.save(user)
        
        # Send welcome email
        self.email_service.send_welcome_email(user["email"], user["username"])
        
        return user
    
    def _generate_user_id(self) -> int:
        """Generate unique user ID."""
        import random
        return random.randint(100000, 999999)

# ✅ Comprehensive test cases
class TestUserService:
    """Test cases for UserService."""
    
    def setup_method(self):
        """Set up test fixtures."""
        self.mock_repository = Mock()
        self.mock_email_service = Mock()
        self.user_service = UserService(self.mock_repository, self.mock_email_service)
    
    def test_create_user_success(self):
        """Test successful user creation."""
        # Arrange
        username = "testuser"
        email = "[email protected]"
        self.mock_repository.find_by_username.return_value = None
        
        # Act
        result = self.user_service.create_user(username, email)
        
        # Assert
        assert result["username"] == username
        assert result["email"] == email
        assert "id" in result
        assert "created_at" in result
        
        # Verify interactions
        self.mock_repository.find_by_username.assert_called_once_with(username)
        self.mock_repository.save.assert_called_once()
        self.mock_email_service.send_welcome_email.assert_called_once_with(email, username)
    
    def test_create_user_duplicate_username(self):
        """Test error when username already exists."""
        # Arrange
        username = "existinguser"
        email = "[email protected]"
        self.mock_repository.find_by_username.return_value = {"id": 123}
        
        # Act & Assert
        with pytest.raises(ValueError, match="Username already exists"):
            self.user_service.create_user(username, email)
        
        # Verify repository was checked but user not saved
        self.mock_repository.find_by_username.assert_called_once_with(username)
        self.mock_repository.save.assert_not_called()
        self.mock_email_service.send_welcome_email.assert_not_called()
    
    @patch('random.randint')
    def test_user_id_generation(self, mock_randint):
        """Test user ID generation."""
        # Arrange
        mock_randint.return_value = 555555
        username = "testuser"
        email = "[email protected]"
        self.mock_repository.find_by_username.return_value = None
        
        # Act
        result = self.user_service.create_user(username, email)
        
        # Assert
        assert result["id"] == 555555
        mock_randint.assert_called_once_with(100000, 999999)

# ✅ Property-based testing with hypothesis
from hypothesis import given, strategies as st

class TestCalculateTax:
    """Property-based tests for tax calculation."""
    
    @given(
        income=st.floats(min_value=0, max_value=1000000, allow_nan=False, allow_infinity=False),
        tax_rate=st.floats(min_value=0, max_value=1, allow_nan=False, allow_infinity=False)
    )
    def test_tax_calculation_properties(self, income: float, tax_rate: float):
        """Test mathematical properties of tax calculation."""
        tax = calculate_tax(income, tax_rate)
        
        # Tax should never be negative
        assert tax >= 0
        
        # Tax should never exceed income
        assert tax <= income
        
        # Tax should be proportional to income
        if income > 0:
            assert abs(tax / income - tax_rate) < 0.0001

Testing Best Practices

  • Arrange-Act-Assert: Structure tests clearly with setup, execution, and verification
  • Mock Dependencies: Isolate units under test from external dependencies
  • Test Edge Cases: Include boundary values and error conditions
  • Property Testing: Use hypothesis for generating test cases automatically