Apply SOLID principles and clean code practices for maintainable, testable Python applications.
Follow SOLID principles to create flexible, maintainable, and testable code.
Each class should have only one reason to change.
# ❌ 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 Classes should be open for extension but closed for modification.
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 Depend on abstractions, not concretions.
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") Write idiomatic Python code that leverages language features effectively.
# ✅ 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) 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 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