Python Best Practices: A Comprehensive Guide
Table of Contents
- Code Style and Formatting
- Naming Conventions
- Function and Class Design
- Error Handling
- Performance Optimization
- Testing Strategies
- Documentation
- 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.