Python Best Practices

Python Best Practices: A Comprehensive Guide

Table of Contents

  1. Code Style and Formatting
  2. Naming Conventions
  3. Function and Class Design
  4. Error Handling
  5. Performance Optimization
  6. Testing Strategies
  7. Documentation
  8. Security Considerations

Code Style and Formatting {#code-style}

PEP 8 Compliance

Python Enhancement Proposal 8 (PEP 8) is the style guide for Python code. Following PEP 8 ensures your code is readable and consistent with the broader Python community.

# Good: Clear spacing and structure
def calculate_compound_interest(principal, rate, time, compound_frequency=1):
    """Calculate compound interest using the standard formula."""
    return principal * (1 + rate / compound_frequency) ** (compound_frequency * time)

# Bad: Poor spacing and structure
def calculate_compound_interest(principal,rate,time,compound_frequency=1):
    return principal*(1+rate/compound_frequency)**(compound_frequency*time)

Line Length and Breaking

Keep lines under 79 characters for code and 72 for comments and docstrings:

# Good: Proper line breaking
result = some_function_with_a_long_name(
    argument_one,
    argument_two,
    argument_three
)

# Good: Breaking long strings
message = (
    "This is a very long string that would exceed the line limit "
    "so we break it into multiple lines for better readability."
)

Import Organization

Organize imports in the following order:
1. Standard library imports
2. Third-party library imports
3. Local application imports

# Standard library
import os
import sys
from datetime import datetime

# Third-party
import requests
import numpy as np
from flask import Flask

# Local
from .models import User
from .utils import helper_function

Naming Conventions {#naming}

Variables and Functions

Use lowercase with underscores (snake_case):

# Good
user_name = "john_doe"
total_amount = 1500.50

def get_user_profile(user_id):
    pass

def calculate_tax_amount(income, tax_rate):
    pass

Classes

Use CapWords (PascalCase):

class UserProfile:
    def __init__(self, username, email):
        self.username = username
        self.email = email

class DatabaseConnection:
    pass

Constants

Use uppercase with underscores:

MAX_RETRY_ATTEMPTS = 3
DEFAULT_TIMEOUT = 30
API_BASE_URL = "https://api.example.com"

Private Members

Use leading underscore for internal use:

class Calculator:
    def __init__(self):
        self._internal_state = {}
        self.__private_data = []  # Name mangling for true privacy

    def _helper_method(self):
        """Internal helper method."""
        pass

Function and Class Design {#design}

Single Responsibility Principle

Each function should do one thing well:

# Good: Single responsibility
def validate_email(email):
    """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

def send_email(to_address, subject, body):
    """Send email to specified address."""
    # Email sending logic here
    pass

# Bad: Multiple responsibilities
def validate_and_send_email(email, subject, body):
    """Validate email and send if valid."""
    # Validation logic
    # Email sending logic
    pass

Function Parameters

Use clear parameter names and provide defaults where appropriate:

def create_user_account(
    username,
    email,
    password,
    is_active=True,
    email_verified=False,
    created_by=None
):
    """Create a new user account with specified parameters."""
    pass

# Using keyword arguments for clarity
create_user_account(
    username="john_doe",
    email="john@example.com",
    password="secure_password",
    is_active=True
)

Class Design Patterns

Builder Pattern

class QueryBuilder:
    def __init__(self):
        self._query = ""
        self._conditions = []
        self._order_by = []

    def select(self, fields):
        self._query = f"SELECT {', '.join(fields)}"
        return self

    def from_table(self, table):
        self._query += f" FROM {table}"
        return self

    def where(self, condition):
        self._conditions.append(condition)
        return self

    def order_by(self, field, direction="ASC"):
        self._order_by.append(f"{field} {direction}")
        return self

    def build(self):
        query = self._query
        if self._conditions:
            query += " WHERE " + " AND ".join(self._conditions)
        if self._order_by:
            query += " ORDER BY " + ", ".join(self._order_by)
        return query

# Usage
query = (QueryBuilder()
         .select(["name", "email", "created_at"])
         .from_table("users")
         .where("is_active = 1")
         .where("email_verified = 1")
         .order_by("created_at", "DESC")
         .build())

Error Handling {#error-handling}

Specific Exception Handling

Catch specific exceptions rather than using bare except clauses:

import requests
from requests.exceptions import ConnectionError, Timeout, RequestException

def fetch_user_data(user_id):
    """Fetch user data from API with proper error handling."""
    try:
        response = requests.get(
            f"https://api.example.com/users/{user_id}",
            timeout=10
        )
        response.raise_for_status()
        return response.json()

    except ConnectionError:
        logger.error("Failed to connect to the API")
        raise UserDataError("Unable to connect to user service")

    except Timeout:
        logger.error("API request timed out")
        raise UserDataError("Request timed out")

    except RequestException as e:
        logger.error(f"API request failed: {e}")
        raise UserDataError(f"Failed to fetch user data: {e}")

Custom Exceptions

Create custom exception classes for your application:

class UserDataError(Exception):
    """Raised when user data operations fail."""
    pass

class ValidationError(Exception):
    """Raised when data validation fails."""
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"Validation error in {field}: {message}")

class ConfigurationError(Exception):
    """Raised when configuration is invalid."""
    pass

Context Managers

Use context managers for resource management:

import sqlite3
from contextlib import contextmanager

@contextmanager
def database_connection(db_path):
    """Context manager for database connections."""
    conn = None
    try:
        conn = sqlite3.connect(db_path)
        yield conn
    except Exception as e:
        if conn:
            conn.rollback()
        raise
    finally:
        if conn:
            conn.close()

# Usage
with database_connection("app.db") as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users")
    results = cursor.fetchall()

Performance Optimization {#performance}

List Comprehensions vs Loops

Use list comprehensions for simple transformations:

# Good: List comprehension
squared_numbers = [x**2 for x in range(1000) if x % 2 == 0]

# Good: Generator expression for memory efficiency
squared_numbers = (x**2 for x in range(1000) if x % 2 == 0)

# Avoid: Traditional loop for simple operations
squared_numbers = []
for x in range(1000):
    if x % 2 == 0:
        squared_numbers.append(x**2)

Dictionary and Set Operations

Leverage O(1) lookup times:

# Good: Using sets for membership testing
valid_statuses = {"active", "pending", "suspended"}
if user_status in valid_statuses:
    process_user()

# Good: Dictionary for mapping
status_messages = {
    "active": "User is active",
    "pending": "User activation pending",
    "suspended": "User account suspended"
}
message = status_messages.get(user_status, "Unknown status")

String Operations

Use join() for string concatenation:

# Good: Using join
parts = ["Hello", "world", "from", "Python"]
message = " ".join(parts)

# Good: f-strings for formatting
name = "Alice"
age = 30
greeting = f"Hello, {name}! You are {age} years old."

# Avoid: String concatenation in loops
message = ""
for part in parts:
    message += part + " "

Caching and Memoization

Use functools.lru_cache for expensive computations:

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    """Calculate Fibonacci number with memoization."""
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

@lru_cache(maxsize=None)
def expensive_calculation(param1, param2):
    """Expensive calculation that benefits from caching."""
    # Complex computation here
    result = param1 ** param2 + complex_algorithm(param1, param2)
    return result

Testing Strategies {#testing}

Unit Testing with pytest

Write comprehensive unit tests:

import pytest
from myapp.calculator import Calculator
from myapp.exceptions import DivisionByZeroError

class TestCalculator:
    def setup_method(self):
        """Set up test fixtures before each test method."""
        self.calculator = Calculator()

    def test_addition(self):
        """Test basic addition functionality."""
        assert self.calculator.add(2, 3) == 5
        assert self.calculator.add(-1, 1) == 0
        assert self.calculator.add(0, 0) == 0

    def test_division_by_zero(self):
        """Test that division by zero raises appropriate exception."""
        with pytest.raises(DivisionByZeroError):
            self.calculator.divide(10, 0)

    @pytest.mark.parametrize("a,b,expected", [
        (2, 3, 6),
        (-2, 3, -6),
        (0, 5, 0),
        (2.5, 4, 10.0)
    ])
    def test_multiplication(self, a, b, expected):
        """Test multiplication with various inputs."""
        assert self.calculator.multiply(a, b) == expected

Mocking External Dependencies

Use unittest.mock for testing:

from unittest.mock import Mock, patch
import requests

def get_weather_data(city):
    """Fetch weather data from external API."""
    response = requests.get(f"http://api.weather.com/{city}")
    return response.json()

class TestWeatherService:
    @patch('requests.get')
    def test_get_weather_data(self, mock_get):
        """Test weather data fetching with mocked API."""
        # Setup mock response
        mock_response = Mock()
        mock_response.json.return_value = {
            "temperature": 25,
            "condition": "sunny"
        }
        mock_get.return_value = mock_response

        # Test the function
        result = get_weather_data("London")

        # Assertions
        assert result["temperature"] == 25
        assert result["condition"] == "sunny"
        mock_get.assert_called_once_with("http://api.weather.com/London")

Documentation {#documentation}

Docstring Conventions

Follow Google or NumPy style docstrings:

def calculate_compound_interest(principal, rate, time, compound_frequency=1):
    """Calculate compound interest using the standard formula.

    Args:
        principal (float): The initial amount of money.
        rate (float): The annual interest rate (as a decimal).
        time (float): The number of years.
        compound_frequency (int, optional): Number of times interest is 
            compounded per year. Defaults to 1.

    Returns:
        float: The final amount after compound interest.

    Raises:
        ValueError: If any of the numeric parameters are negative.
        TypeError: If parameters are not numeric types.

    Example:
        >>> calculate_compound_interest(1000, 0.05, 10, 12)
        1643.62
    """
    if principal < 0 or rate < 0 or time < 0:
        raise ValueError("All parameters must be non-negative")

    return principal * (1 + rate / compound_frequency) ** (compound_frequency * time)

Type Hints

Use type hints for better code documentation and IDE support:

from typing import List, Dict, Optional, Union, Callable
from dataclasses import dataclass

@dataclass
class User:
    """User data class with type annotations."""
    id: int
    username: str
    email: str
    is_active: bool = True
    metadata: Optional[Dict[str, str]] = None

def process_users(
    users: List[User],
    filter_func: Callable[[User], bool],
    active_only: bool = True
) -> List[Dict[str, Union[str, int, bool]]]:
    """Process a list of users with optional filtering.

    Args:
        users: List of User objects to process.
        filter_func: Function to filter users.
        active_only: Whether to include only active users.

    Returns:
        List of user dictionaries.
    """
    filtered_users = [
        user for user in users 
        if (not active_only or user.is_active) and filter_func(user)
    ]

    return [
        {
            "id": user.id,
            "username": user.username,
            "email": user.email,
            "is_active": user.is_active
        }
        for user in filtered_users
    ]

Security Considerations {#security}

Input Validation and Sanitization

Always validate and sanitize user input:

import re
from html import escape

def validate_username(username: str) -> bool:
    """Validate username format and length."""
    if not isinstance(username, str):
        return False

    if len(username) < 3 or len(username) > 30:
        return False

    # Allow only alphanumeric characters and underscores
    pattern = r'^[a-zA-Z0-9_]+$'
    return bool(re.match(pattern, username))

def sanitize_html_input(user_input: str) -> str:
    """Sanitize HTML input to prevent XSS attacks."""
    return escape(user_input.strip())

def validate_email_format(email: str) -> bool:
    """Validate email format."""
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))

Password Security

Implement secure password handling:

import hashlib
import secrets
from cryptography.fernet import Fernet

def generate_salt() -> bytes:
    """Generate a random salt for password hashing."""
    return secrets.token_bytes(32)

def hash_password(password: str, salt: bytes) -> bytes:
    """Hash password with salt using PBKDF2."""
    return hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)

def verify_password(password: str, salt: bytes, hashed: bytes) -> bool:
    """Verify password against stored hash."""
    return hash_password(password, salt) == hashed

class SecureStorage:
    """Secure storage for sensitive data."""

    def __init__(self, key: bytes = None):
        self.key = key or Fernet.generate_key()
        self.cipher = Fernet(self.key)

    def encrypt(self, data: str) -> bytes:
        """Encrypt sensitive data."""
        return self.cipher.encrypt(data.encode())

    def decrypt(self, encrypted_data: bytes) -> str:
        """Decrypt sensitive data."""
        return self.cipher.decrypt(encrypted_data).decode()

Environment Variables for Secrets

Never hardcode secrets in your code:

import os
from typing import Optional

def get_database_url() -> str:
    """Get database URL from environment variables."""
    db_url = os.getenv('DATABASE_URL')
    if not db_url:
        raise ValueError("DATABASE_URL environment variable is required")
    return db_url

def get_api_key(service_name: str) -> str:
    """Get API key for specified service."""
    key = os.getenv(f'{service_name.upper()}_API_KEY')
    if not key:
        raise ValueError(f"{service_name} API key not found in environment")
    return key

# Configuration class
class Config:
    """Application configuration from environment variables."""

    DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///app.db')
    SECRET_KEY = os.getenv('SECRET_KEY') or secrets.token_hex(32)
    DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
    API_RATE_LIMIT = int(os.getenv('API_RATE_LIMIT', '1000'))

This comprehensive guide covers the essential Python best practices that every developer should follow. By implementing these practices, you'll write more maintainable, secure, and efficient Python code that follows industry standards and community conventions.

Remember that best practices evolve with the language and community. Stay updated with the latest Python Enhancement Proposals (PEPs) and continue learning from the Python community to keep your skills sharp and your code exemplary.