Skip to content

Best Practices

Beyond style guides and tools, these principles will help you write cleaner, more maintainable code.

Core Principles

1. DRY (Don't Repeat Yourself)

Avoid duplicating code. Extract repeated logic into functions or classes.

# Bad - repetitive code
def calculate_circle_area(radius):
    return 3.14159 * radius * radius

def calculate_circle_circumference(radius):
    return 2 * 3.14159 * radius

def calculate_sphere_volume(radius):
    return (4/3) * 3.14159 * radius * radius * radius

# Good - extract constants and common patterns
import math

PI = math.pi

def calculate_circle_area(radius: float) -> float:
    """Calculate the area of a circle."""
    return PI * radius ** 2

def calculate_circle_circumference(radius: float) -> float:
    """Calculate the circumference of a circle."""
    return 2 * PI * radius

def calculate_sphere_volume(radius: float) -> float:
    """Calculate the volume of a sphere."""
    return (4/3) * PI * radius ** 3

2. KISS (Keep It Simple, Stupid)

Write simple, straightforward code. Avoid over-engineering.

# Bad - overly complex
def is_even_complex(number):
    return True if number % 2 == 0 else False

def get_user_status_complex(user):
    if user.is_active == True:
        if user.subscription_expired == False:
            return "active"
        else:
            return "expired"
    else:
        return "inactive"

# Good - simple and clear
def is_even(number: int) -> bool:
    """Check if a number is even."""
    return number % 2 == 0

def get_user_status(user) -> str:
    """Get user status based on activity and subscription."""
    if not user.is_active:
        return "inactive"
    if user.subscription_expired:
        return "expired"
    return "active"

3. YAGNI (You Aren't Gonna Need It)

Don't add functionality until it's necessary.

# Bad - premature optimization and unused features
class DataProcessor:
    def __init__(self):
        self.cache = {}  # Might need caching later
        self.parallel_processing = False  # Future feature
        self.compression_enabled = False  # Maybe useful

    def process(self, data):
        # Only actually need this simple processing
        return [item.strip().lower() for item in data]

# Good - implement only what's needed now
class DataProcessor:
    def process(self, data: list[str]) -> list[str]:
        """Process data by stripping whitespace and converting to lowercase."""
        return [item.strip().lower() for item in data]

Code Organization

4. Single Responsibility Principle

Each function or class should have one reason to change.

# Bad - multiple responsibilities
class UserManager:
    def create_user(self, user_data):
        # Validate data
        if not user_data.get('email'):
            raise ValueError("Email required")

        # Save to database
        db.save_user(user_data)

        # Send welcome email
        email_service.send_welcome(user_data['email'])

        # Log the action
        logger.info(f"User created: {user_data['email']}")

# Good - separated responsibilities
class UserValidator:
    def validate(self, user_data: dict) -> None:
        """Validate user data."""
        if not user_data.get('email'):
            raise ValueError("Email required")

class UserRepository:
    def save(self, user_data: dict) -> None:
        """Save user to database."""
        db.save_user(user_data)

class UserNotificationService:
    def send_welcome_email(self, email: str) -> None:
        """Send welcome email to new user."""
        email_service.send_welcome(email)

class UserManager:
    def __init__(self):
        self.validator = UserValidator()
        self.repository = UserRepository()
        self.notification_service = UserNotificationService()

    def create_user(self, user_data: dict) -> None:
        """Create a new user."""
        self.validator.validate(user_data)
        self.repository.save(user_data)
        self.notification_service.send_welcome_email(user_data['email'])
        logger.info(f"User created: {user_data['email']}")

5. Write Self-Documenting Code

Choose descriptive names that explain their purpose.

# Bad - unclear names
def calc(d, r):
    return d * r * 0.1

def proc_usr(u):
    if u[0] > 18:
        return True
    return False

# Good - self-documenting
def calculate_discount(price: float, discount_rate: float) -> float:
    """Calculate discount amount."""
    return price * discount_rate * 0.1

def is_adult(user_age: int) -> bool:
    """Check if user is an adult (18 or older)."""
    return user_age >= 18

Error Handling

6. Fail Fast and Explicitly

Catch errors early and provide clear error messages.

# Bad - silent failures and unclear errors
def divide_numbers(a, b):
    try:
        return a / b
    except:
        return None

def process_file(filename):
    try:
        with open(filename) as f:
            return f.read()
    except:
        return ""

# Good - explicit error handling
def divide_numbers(a: float, b: float) -> float:
    """Divide two numbers."""
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

def process_file(filename: str) -> str:
    """Read and return file contents."""
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            return f.read()
    except FileNotFoundError:
        raise FileNotFoundError(f"File not found: {filename}")
    except PermissionError:
        raise PermissionError(f"Permission denied: {filename}")
    except UnicodeDecodeError:
        raise UnicodeDecodeError(f"Cannot decode file: {filename}")

7. Use Context Managers

Ensure proper resource cleanup.

# Bad - manual resource management
def read_config():
    file = open('config.txt')
    data = file.read()
    file.close()  # Might not execute if error occurs
    return data

# Good - automatic resource management
def read_config() -> str:
    """Read configuration from file."""
    with open('config.txt', 'r', encoding='utf-8') as file:
        return file.read()

# Custom context manager for database connections
from contextlib import contextmanager

@contextmanager
def database_connection():
    """Context manager for database connections."""
    conn = create_connection()
    try:
        yield conn
    finally:
        conn.close()

# Usage
def get_user_data(user_id: int) -> dict:
    """Get user data from database."""
    with database_connection() as conn:
        return conn.execute("SELECT * FROM users WHERE id = ?", (user_id,))

Function Design

8. Keep Functions Small

Functions should do one thing well.

# Bad - large function doing multiple things
def process_user_registration(user_data):
    # Validate email
    if '@' not in user_data['email']:
        raise ValueError("Invalid email")

    # Check if user exists
    existing = db.query("SELECT * FROM users WHERE email = ?", user_data['email'])
    if existing:
        raise ValueError("User already exists")

    # Hash password
    import hashlib
    hashed_password = hashlib.sha256(user_data['password'].encode()).hexdigest()

    # Save user
    db.execute("INSERT INTO users (email, password) VALUES (?, ?)",
               user_data['email'], hashed_password)

    # Send email
    send_email(user_data['email'], "Welcome!")

    # Log action
    logger.info(f"User registered: {user_data['email']}")

# Good - small, focused functions
def validate_email(email: str) -> None:
    """Validate email format."""
    if '@' not in email:
        raise ValueError("Invalid email format")

def check_user_exists(email: str) -> bool:
    """Check if user already exists."""
    existing = db.query("SELECT * FROM users WHERE email = ?", email)
    return bool(existing)

def hash_password(password: str) -> str:
    """Hash password using SHA256."""
    import hashlib
    return hashlib.sha256(password.encode()).hexdigest()

def save_user(email: str, hashed_password: str) -> None:
    """Save user to database."""
    db.execute("INSERT INTO users (email, password) VALUES (?, ?)",
               email, hashed_password)

def process_user_registration(user_data: dict) -> None:
    """Process user registration."""
    validate_email(user_data['email'])

    if check_user_exists(user_data['email']):
        raise ValueError("User already exists")

    hashed_password = hash_password(user_data['password'])
    save_user(user_data['email'], hashed_password)

    send_email(user_data['email'], "Welcome!")
    logger.info(f"User registered: {user_data['email']}")

9. Use Pure Functions When Possible

Functions that don't have side effects are easier to test and reason about.

# Bad - function with side effects
total_processed = 0

def process_item(item):
    global total_processed
    result = item.upper()
    total_processed += 1  # Side effect
    print(f"Processed: {result}")  # Side effect
    return result

# Good - pure function
def process_item(item: str) -> str:
    """Process item by converting to uppercase."""
    return item.upper()

def process_items_with_logging(items: list[str]) -> list[str]:
    """Process items and log progress."""
    results = []
    for i, item in enumerate(items):
        result = process_item(item)
        results.append(result)
        print(f"Processed {i+1}/{len(items)}: {result}")
    return results

Data Structures

10. Use Appropriate Data Structures

Choose the right data structure for the job.

# Bad - using list for lookups
def find_user_by_id(users_list, user_id):
    for user in users_list:  # O(n) lookup
        if user['id'] == user_id:
            return user
    return None

# Good - using dictionary for O(1) lookups
def create_user_index(users: list[dict]) -> dict[int, dict]:
    """Create an index of users by ID for fast lookups."""
    return {user['id']: user for user in users}

def find_user_by_id(user_index: dict[int, dict], user_id: int) -> dict | None:
    """Find user by ID using index."""
    return user_index.get(user_id)

# Use sets for membership testing
def filter_allowed_users(users: list[str], allowed_users: set[str]) -> list[str]:
    """Filter users to only include allowed ones."""
    return [user for user in users if user in allowed_users]  # O(1) lookup

11. Prefer Immutable Data

Immutable data is safer and easier to reason about.

# Bad - mutable default arguments
def add_item(item, items=[]):  # Dangerous!
    items.append(item)
    return items

# Good - immutable approach
def add_item(item: str, items: list[str] | None = None) -> list[str]:
    """Add item to list, returning new list."""
    if items is None:
        items = []
    return items + [item]  # Return new list

# Use dataclasses with frozen=True for immutable objects
from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: float
    y: float

    def move(self, dx: float, dy: float) -> 'Point':
        """Return new Point moved by dx, dy."""
        return Point(self.x + dx, self.y + dy)

Testing Considerations

12. Write Testable Code

Design code to be easy to test.

# Bad - hard to test due to dependencies
def process_user_data():
    data = requests.get("https://api.example.com/users").json()
    processed = [user['name'].upper() for user in data]
    with open('output.txt', 'w') as f:
        f.write('\n'.join(processed))

# Good - testable with dependency injection
def fetch_user_data(api_client) -> list[dict]:
    """Fetch user data from API."""
    return api_client.get_users()

def process_names(users: list[dict]) -> list[str]:
    """Process user names."""
    return [user['name'].upper() for user in users]

def save_results(data: list[str], file_writer) -> None:
    """Save results using provided writer."""
    file_writer.write('\n'.join(data))

def process_user_data(api_client, file_writer) -> None:
    """Main processing function."""
    users = fetch_user_data(api_client)
    processed_names = process_names(users)
    save_results(processed_names, file_writer)

Performance Considerations

13. Optimize When Necessary

Don't optimize prematurely, but be aware of performance implications.

# Bad - premature optimization
def find_max_optimized(numbers):
    # Unnecessary complexity for most use cases
    if not numbers:
        return None
    max_val = numbers[0]
    for i in range(1, len(numbers)):
        if numbers[i] > max_val:
            max_val = numbers[i]
    return max_val

# Good - use built-in functions first
def find_max(numbers: list[float]) -> float | None:
    """Find maximum value in list."""
    return max(numbers) if numbers else None

# Optimize only when profiling shows it's needed
def find_max_large_dataset(numbers: list[float]) -> float | None:
    """Optimized max finding for very large datasets."""
    if not numbers:
        return None

    # Use numpy for large datasets if available
    try:
        import numpy as np
        return float(np.max(numbers))
    except ImportError:
        return max(numbers)

These best practices will help you write code that is not only functional but also maintainable, readable, and robust. Remember: good code is written for humans to read, not just for computers to execute.