Python Best Practices
Master Python best practices including PEP 8 style guide, code organization, testing, documentation, performance optimization, and security principles.
PEP 8 Style Guide
PEP 8 is Python's official style guide. Following it ensures your code is readable and consistent:
# Good: Clear, readable code following PEP 8
def calculate_total_price(items, tax_rate=0.08):
"""
Calculate total price including tax.
Args:
items (list): List of item prices
tax_rate (float): Tax rate as decimal (default 8%)
Returns:
float: Total price including tax
"""
subtotal = sum(items)
tax = subtotal * tax_rate
total = subtotal + tax
return round(total, 2)
# Bad: Poor naming, no documentation, inconsistent spacing
def calc(items,tax=0.08):
s=sum(items)
t=s*tax
return s+t
# Naming conventions
# Variables and functions: snake_case
user_name = "john_doe"
def get_user_data():
pass
# Classes: PascalCase
class UserProfile:
pass
# Constants: UPPER_CASE
MAX_CONNECTIONS = 100
DEFAULT_TIMEOUT = 30
# Private attributes/methods: leading underscore
class DatabaseConnection:
def __init__(self):
self._connection = None
def _validate_connection(self):
pass
# Indentation: 4 spaces (never tabs)
def example_function():
if True:
print("This is properly indented")
if False:
print("Nested indentation")
# Line length: Maximum 79 characters for code, 72 for docstrings
# Bad (too long):
result = some_very_long_function_name_that_makes_this_line_exceed_the_recommended_length(parameter_one, parameter_two)
# Good (broken into multiple lines):
result = some_very_long_function_name(
parameter_one,
parameter_two
)
# Or use backslashes for line continuation:
result = some_very_long_function_name( \
parameter_one, \
parameter_two \
)
# Blank lines: Use blank lines to separate logical sections
class Calculator:
def __init__(self):
self.result = 0
def add(self, value):
self.result += value
return self.result
def multiply(self, value):
self.result *= value
return self.result
# Import organization
# Standard library imports first
import os
import sys
from pathlib import Path
# Then third-party imports
import requests
from flask import Flask
# Finally local imports
from .models import User
from ..utils import helper_function
# Avoid wildcard imports
# Bad:
from math import *
# Good:
from math import sqrt, pi, sin, cos
# String quotes: Use double quotes for strings, single for characters
name = "John Doe"
initial = 'J'
# But be consistent within a project
Code Organization and Structure
Organizing your Python projects for maintainability:
# Project structure example
my_project/
├── README.md # Project description and setup instructions
├── requirements.txt # Python dependencies
├── setup.py # Package setup (for distribution)
├── .gitignore # Git ignore rules
├── .env # Environment variables (don't commit)
├── docs/ # Documentation
│ ├── index.md
│ └── api.md
├── tests/ # Unit and integration tests
│ ├── __init__.py
│ ├── test_models.py
│ └── test_views.py
├── src/ # Source code
│ ├── __init__.py
│ ├── main.py # Entry point
│ ├── config.py # Configuration management
│ ├── database.py # Database connections/utilities
│ ├── models/ # Data models
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── product.py
│ ├── services/ # Business logic
│ │ ├── __init__.py
│ │ ├── user_service.py
│ │ └── payment_service.py
│ ├── utils/ # Utility functions
│ │ ├── __init__.py
│ │ ├── validators.py
│ │ └── formatters.py
│ └── api/ # API endpoints
│ ├── __init__.py
│ ├── routes.py
│ └── middleware.py
└── scripts/ # Utility scripts
├── setup_database.py
└── migrate_data.py
# config.py - Configuration management
import os
from dataclasses import dataclass
from typing import Optional
@dataclass
class DatabaseConfig:
host: str
port: int
name: str
user: str
password: str
@property
def connection_string(self) -> str:
return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.name}"
@dataclass
class AppConfig:
debug: bool
secret_key: str
database: DatabaseConfig
@classmethod
def from_env(cls) -> 'AppConfig':
return cls(
debug=os.getenv('DEBUG', 'False').lower() == 'true',
secret_key=os.getenv('SECRET_KEY', 'dev-secret-key'),
database=DatabaseConfig(
host=os.getenv('DB_HOST', 'localhost'),
port=int(os.getenv('DB_PORT', '5432')),
name=os.getenv('DB_NAME', 'myapp'),
user=os.getenv('DB_USER', 'postgres'),
password=os.getenv('DB_PASSWORD', '')
)
)
# Usage
config = AppConfig.from_env()
# main.py - Entry point
from src.config import AppConfig
from src.database import init_database
from src.api.routes import create_app
def main():
config = AppConfig.from_env()
# Initialize database
init_database(config.database)
# Create and run application
app = create_app(config)
app.run(
host='0.0.0.0',
port=8000,
debug=config.debug
)
if __name__ == '__main__':
main()
# models/user.py - Model definition
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
from email_validator import validate_email, EmailNotValidError
@dataclass
class User:
id: Optional[int]
email: str
username: str
full_name: str
created_at: datetime
updated_at: datetime
is_active: bool = True
def __post_init__(self):
# Validate email
try:
validate_email(self.email)
except EmailNotValidError:
raise ValueError(f"Invalid email: {self.email}")
# Validate username
if not self.username or len(self.username) < 3:
raise ValueError("Username must be at least 3 characters long")
@property
def display_name(self) -> str:
return self.full_name or self.username
def to_dict(self) -> dict:
return {
'id': self.id,
'email': self.email,
'username': self.username,
'full_name': self.full_name,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
'is_active': self.is_active,
}
# services/user_service.py - Business logic
from typing import List, Optional
from src.models.user import User
from src.database import DatabaseConnection
class UserService:
def __init__(self, db: DatabaseConnection):
self.db = db
def create_user(self, email: str, username: str, full_name: str) -> User:
"""Create a new user."""
# Check if user already exists
if self.get_user_by_email(email):
raise ValueError("User with this email already exists")
if self.get_user_by_username(username):
raise ValueError("Username already taken")
user = User(
id=None,
email=email,
username=username,
full_name=full_name,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
)
# Save to database
user.id = self.db.insert_user(user)
return user
def get_user_by_id(self, user_id: int) -> Optional[User]:
"""Get user by ID."""
return self.db.get_user_by_id(user_id)
def get_user_by_email(self, email: str) -> Optional[User]:
"""Get user by email."""
return self.db.get_user_by_email(email)
def get_user_by_username(self, username: str) -> Optional[User]:
"""Get user by username."""
return self.db.get_user_by_username(username)
def update_user(self, user_id: int, **updates) -> Optional[User]:
"""Update user information."""
user = self.get_user_by_id(user_id)
if not user:
return None
# Apply updates
for key, value in updates.items():
if hasattr(user, key):
setattr(user, key, value)
user.updated_at = datetime.utcnow()
self.db.update_user(user)
return user
def delete_user(self, user_id: int) -> bool:
"""Delete a user."""
return self.db.delete_user(user_id)
def list_users(self, limit: int = 50, offset: int = 0) -> List[User]:
"""List users with pagination."""
return self.db.list_users(limit, offset)
# utils/validators.py - Utility functions
import re
from typing import Optional
def validate_email_format(email: str) -> bool:
"""Validate email format using regex."""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
def validate_password_strength(password: str) -> tuple[bool, Optional[str]]:
"""Validate password strength."""
if len(password) < 8:
return False, "Password must be at least 8 characters long"
if not re.search(r'[A-Z]', password):
return False, "Password must contain at least one uppercase letter"
if not re.search(r'[a-z]', password):
return False, "Password must contain at least one lowercase letter"
if not re.search(r'\d', password):
return False, "Password must contain at least one digit"
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
return False, "Password must contain at least one special character"
return True, None
def sanitize_input(text: str) -> str:
"""Sanitize user input to prevent XSS."""
# Remove potentially dangerous characters
dangerous_chars = ['<', '>', '&', '"', "'"]
for char in dangerous_chars:
text = text.replace(char, '')
return text.strip()
Testing Best Practices
Writing comprehensive tests for your Python code:
# tests/test_models.py
import pytest
from datetime import datetime
from src.models.user import User
class TestUser:
def test_user_creation_valid_data(self):
"""Test creating a user with valid data."""
user = User(
id=None,
email="john@example.com",
username="johndoe",
full_name="John Doe",
created_at=datetime(2023, 1, 1, 12, 0, 0),
updated_at=datetime(2023, 1, 1, 12, 0, 0)
)
assert user.email == "john@example.com"
assert user.username == "johndoe"
assert user.display_name == "John Doe"
assert user.is_active == True
def test_user_creation_invalid_email(self):
"""Test that invalid email raises ValueError."""
with pytest.raises(ValueError, match="Invalid email"):
User(
id=None,
email="invalid-email",
username="johndoe",
full_name="John Doe",
created_at=datetime(2023, 1, 1, 12, 0, 0),
updated_at=datetime(2023, 1, 1, 12, 0, 0)
)
def test_user_creation_short_username(self):
"""Test that short username raises ValueError."""
with pytest.raises(ValueError, match="Username must be at least 3 characters"):
User(
id=None,
email="john@example.com",
username="jo",
full_name="John Doe",
created_at=datetime(2023, 1, 1, 12, 0, 0),
updated_at=datetime(2023, 1, 1, 12, 0, 0)
)
def test_user_to_dict(self):
"""Test converting user to dictionary."""
user = User(
id=1,
email="john@example.com",
username="johndoe",
full_name="John Doe",
created_at=datetime(2023, 1, 1, 12, 0, 0),
updated_at=datetime(2023, 1, 1, 12, 0, 0)
)
user_dict = user.to_dict()
assert user_dict['id'] == 1
assert user_dict['email'] == "john@example.com"
assert user_dict['username'] == "johndoe"
assert 'created_at' in user_dict
assert 'updated_at' in user_dict
# tests/test_user_service.py
import pytest
from unittest.mock import Mock, MagicMock
from src.services.user_service import UserService
from src.models.user import User
class TestUserService:
@pytest.fixture
def mock_db(self):
"""Create a mock database connection."""
db = Mock()
db.get_user_by_email.return_value = None
db.get_user_by_username.return_value = None
db.insert_user.return_value = 1
return db
@pytest.fixture
def user_service(self, mock_db):
"""Create a UserService instance with mock database."""
return UserService(mock_db)
def test_create_user_success(self, user_service, mock_db):
"""Test successful user creation."""
user = user_service.create_user(
email="john@example.com",
username="johndoe",
full_name="John Doe"
)
assert user.email == "john@example.com"
assert user.username == "johndoe"
assert user.id == 1
mock_db.insert_user.assert_called_once()
def test_create_user_duplicate_email(self, user_service, mock_db):
"""Test creating user with duplicate email."""
# Setup mock to return existing user
existing_user = Mock()
mock_db.get_user_by_email.return_value = existing_user
with pytest.raises(ValueError, match="User with this email already exists"):
user_service.create_user(
email="john@example.com",
username="johndoe",
full_name="John Doe"
)
def test_get_user_by_id(self, user_service, mock_db):
"""Test getting user by ID."""
expected_user = Mock()
mock_db.get_user_by_id.return_value = expected_user
result = user_service.get_user_by_id(1)
assert result == expected_user
mock_db.get_user_by_id.assert_called_once_with(1)
# conftest.py - Shared test fixtures
import pytest
from src.config import AppConfig, DatabaseConfig
@pytest.fixture
def test_config():
"""Test configuration."""
return AppConfig(
debug=True,
secret_key="test-secret-key",
database=DatabaseConfig(
host="localhost",
port=5432,
name="test_db",
user="test_user",
password="test_password"
)
)
# pytest.ini - Pytest configuration
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--verbose
--tb=short
--cov=src
--cov-report=html
--cov-report=term-missing
--cov-fail-under=80
# Integration tests
# tests/test_integration.py
import pytest
from src.services.user_service import UserService
from src.database import DatabaseConnection
@pytest.mark.integration
class TestUserServiceIntegration:
@pytest.fixture
def db_connection(self, test_config):
"""Real database connection for integration tests."""
db = DatabaseConnection(test_config.database)
# Setup test database
db.create_tables()
yield db
# Cleanup
db.drop_tables()
def test_full_user_lifecycle(self, db_connection):
"""Test complete user lifecycle."""
service = UserService(db_connection)
# Create user
user = service.create_user(
email="integration@example.com",
username="integration_user",
full_name="Integration Test User"
)
assert user.id is not None
# Retrieve user
retrieved = service.get_user_by_id(user.id)
assert retrieved.email == user.email
# Update user
updated = service.update_user(user.id, full_name="Updated Name")
assert updated.full_name == "Updated Name"
# Delete user
result = service.delete_user(user.id)
assert result == True
# Verify deletion
deleted = service.get_user_by_id(user.id)
assert deleted is None
# Performance tests
# tests/test_performance.py
import time
import pytest
from src.services.user_service import UserService
@pytest.mark.performance
class TestUserServicePerformance:
def test_bulk_user_creation_performance(self, user_service):
"""Test performance of bulk user creation."""
start_time = time.time()
users = []
for i in range(100):
user = user_service.create_user(
email=f"user{i}@example.com",
username=f"user{i}",
full_name=f"User {i}"
)
users.append(user)
end_time = time.time()
duration = end_time - start_time
# Should complete within reasonable time
assert duration < 5.0 # 5 seconds max
assert len(users) == 100
Documentation Best Practices
Writing comprehensive documentation:
# Example of well-documented code
"""
User Management System
This module provides functionality for managing users in the application.
It includes user creation, authentication, and profile management.
Classes:
UserManager: Main class for user operations
AuthenticationService: Handles user authentication
Functions:
create_user: Create a new user account
authenticate_user: Verify user credentials
Examples:
>>> manager = UserManager()
>>> user = manager.create_user("john@example.com", "password123")
>>> authenticated = authenticate_user("john@example.com", "password123")
True
"""
from typing import Optional, Dict, Any
from datetime import datetime
class UserManager:
"""
Manages user accounts and operations.
This class provides methods for creating, updating, and managing
user accounts in the system.
Attributes:
db_connection: Database connection object
cache: Optional cache for performance optimization
"""
def __init__(self, db_connection, cache=None):
"""
Initialize the UserManager.
Args:
db_connection: Database connection instance
cache: Optional cache instance for performance
"""
self.db_connection = db_connection
self.cache = cache
def create_user(self, email: str, password: str, **user_data) -> Dict[str, Any]:
"""
Create a new user account.
This method creates a new user with the provided email and password,
along with any additional user data.
Args:
email (str): User's email address
password (str): User's password (will be hashed)
**user_data: Additional user information (name, etc.)
Returns:
Dict[str, Any]: User data including generated ID
Raises:
ValueError: If email is invalid or already exists
DatabaseError: If database operation fails
Example:
>>> user = manager.create_user(
... "john@example.com",
... "secure_password",
... full_name="John Doe"
... )
>>> print(user['id'])
123
"""
# Validate email format
if not self._is_valid_email(email):
raise ValueError("Invalid email format")
# Check if user already exists
if self.get_user_by_email(email):
raise ValueError("User with this email already exists")
# Hash password
hashed_password = self._hash_password(password)
# Create user record
user_record = {
'email': email,
'password_hash': hashed_password,
'created_at': datetime.utcnow(),
'is_active': True,
**user_data
}
# Save to database
user_id = self.db_connection.insert('users', user_record)
user_record['id'] = user_id
# Cache user data
if self.cache:
self.cache.set(f"user:{user_id}", user_record)
return user_record
def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]:
"""
Retrieve user by email address.
Args:
email (str): Email address to search for
Returns:
Optional[Dict[str, Any]]: User data if found, None otherwise
"""
# Check cache first
if self.cache:
cached_user = self.cache.get(f"user:email:{email}")
if cached_user:
return cached_user
# Query database
user = self.db_connection.query_one(
"SELECT * FROM users WHERE email = %s",
(email,)
)
# Cache result
if self.cache and user:
self.cache.set(f"user:email:{email}", user)
return user
def _is_valid_email(self, email: str) -> bool:
"""
Validate email format using regex.
Args:
email (str): Email to validate
Returns:
bool: True if valid, False otherwise
"""
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
def _hash_password(self, password: str) -> str:
"""
Hash password using secure hashing algorithm.
Args:
password (str): Plain text password
Returns:
str: Hashed password
"""
from werkzeug.security import generate_password_hash
return generate_password_hash(password)
def authenticate_user(email: str, password: str) -> bool:
"""
Authenticate a user with email and password.
Args:
email (str): User's email address
password (str): User's password
Returns:
bool: True if authentication successful, False otherwise
Note:
This function uses constant-time comparison to prevent
timing attacks.
"""
manager = UserManager.get_instance()
user = manager.get_user_by_email(email)
if not user or not user.get('is_active', False):
return False
from werkzeug.security import check_password_hash
return check_password_hash(user['password_hash'], password)
# README.md example
# My Python Project
A comprehensive user management system built with Python.
## Features
- User registration and authentication
- Profile management
- Password hashing with Werkzeug
- Database abstraction layer
- Comprehensive test suite
- RESTful API endpoints
## Installation
```bash
pip install -r requirements.txt
```
## Usage
```python
from user_manager import UserManager
# Create a user manager instance
manager = UserManager(database_connection)
# Create a new user
user = manager.create_user("john@example.com", "secure_password")
print(f"Created user with ID: {user['id']}")
```
## API Documentation
See [docs/api.md](docs/api.md) for detailed API documentation.
## Testing
Run the test suite:
```bash
pytest
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Write tests for new functionality
4. Ensure all tests pass
5. Submit a pull request
## License
MIT License - see [LICENSE](LICENSE) file for details.
Performance Optimization
Optimizing Python code for better performance:
# Performance optimization techniques
# 1. Use built-in functions and data structures
# Bad: Manual loop
def sum_list_bad(numbers):
total = 0
for num in numbers:
total += num
return total
# Good: Use built-in sum()
def sum_list_good(numbers):
return sum(numbers)
# 2. List comprehensions vs loops
# Bad: Traditional loop
squares_bad = []
for i in range(1000):
squares_bad.append(i ** 2)
# Good: List comprehension
squares_good = [i ** 2 for i in range(1000)]
# Even better: Generator expression for memory efficiency
squares_generator = (i ** 2 for i in range(1000))
# 3. String concatenation
# Bad: Using + in loop
result = ""
for word in words:
result += word + " "
# Good: Use join()
result = " ".join(words)
# 4. Dictionary lookups vs list searches
# Bad: Linear search in list
def find_user_bad(users, user_id):
for user in users:
if user['id'] == user_id:
return user
return None
# Good: Dictionary lookup
def find_user_good(users_dict, user_id):
return users_dict.get(user_id)
# 5. Caching expensive operations
from functools import lru_cache
import time
@lru_cache(maxsize=128)
def expensive_computation(n):
"""Expensive computation with caching."""
time.sleep(0.1) # Simulate expensive operation
return n * n
# 6. Use appropriate data structures
# For membership testing: set > list
# For ordered data: list > set
# For key-value pairs: dict
# 7. Avoid global variables in performance-critical code
# Bad: Global variable
counter = 0
def increment_bad():
global counter
counter += 1
# Good: Local variables or class attributes
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
# 8. Profile your code to find bottlenecks
import cProfile
import pstats
def profile_function():
# Code to profile
result = sum(i ** 2 for i in range(10000))
return result
# Profile the function
profiler = cProfile.Profile()
profiler.enable()
result = profile_function()
profiler.disable()
# Print statistics
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats()
# 9. Use multiprocessing for CPU-bound tasks
import multiprocessing as mp
from concurrent.futures import ProcessPoolExecutor
def cpu_intensive_task(n):
"""CPU-intensive computation."""
return sum(i * i for i in range(n))
def parallel_processing():
numbers = [1000000] * 4 # 4 tasks
# Sequential (slow)
start = time.time()
results_seq = [cpu_intensive_task(n) for n in numbers]
seq_time = time.time() - start
# Parallel (fast)
start = time.time()
with ProcessPoolExecutor() as executor:
results_par = list(executor.map(cpu_intensive_task, numbers))
par_time = time.time() - start
print(f"Sequential: {seq_time:.2f}s")
print(f"Parallel: {par_time:.2f}s")
print(f"Speedup: {seq_time/par_time:.2f}x")
# 10. Memory optimization
# Use __slots__ for memory-efficient classes
class MemoryEfficientClass:
__slots__ = ['x', 'y', 'z']
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
# Use generators for large datasets
def read_large_file(file_path):
"""Read large file line by line."""
with open(file_path, 'r') as file:
for line in file:
yield line.strip()
# Process large file without loading into memory
for line in read_large_file('large_file.txt'):
process_line(line)
# 11. Database optimization
# Use select_related and prefetch_related for Django
# Use database indexes appropriately
# Batch database operations
# 12. Algorithm optimization
# Choose appropriate algorithms and data structures
# O(n²) vs O(n log n) vs O(n)
# 13. Use compiled extensions for performance-critical code
# Consider using NumPy for numerical computations
# Use Cython for CPU-intensive Python code
# 14. Asynchronous programming for I/O-bound tasks
import asyncio
import aiohttp
async def fetch_url(session, url):
"""Asynchronously fetch a URL."""
async with session.get(url) as response:
return await response.text()
async def fetch_multiple_urls(urls):
"""Fetch multiple URLs concurrently."""
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
return await asyncio.gather(*tasks)
# 15. Code profiling decorators
def timing_decorator(func):
"""Decorator to time function execution."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timing_decorator
def slow_function():
time.sleep(1)
return "Done"
Security Best Practices
Writing secure Python code:
# Security best practices
# 1. Input validation and sanitization
def validate_and_sanitize_input(user_input):
"""Validate and sanitize user input."""
# Remove potentially dangerous characters
sanitized = user_input.replace('<', '<').replace('>', '>')
# Validate length
if len(sanitized) > 1000:
raise ValueError("Input too long")
# Validate against whitelist pattern
import re
if not re.match(r'^[a-zA-Z0-9\s\-_.]+$', sanitized):
raise ValueError("Invalid characters in input")
return sanitized
# 2. Secure password handling
import hashlib
import secrets
import bcrypt
def hash_password(password):
"""Hash password securely."""
# Use bcrypt for password hashing
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode('utf-8'), salt)
def verify_password(password, hashed):
"""Verify password against hash."""
return bcrypt.checkpw(password.encode('utf-8'), hashed)
def generate_secure_token():
"""Generate a secure random token."""
return secrets.token_urlsafe(32)
# 3. SQL injection prevention
# Bad: String formatting (vulnerable to SQL injection)
def get_user_bad(cursor, user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
cursor.execute(query)
# Good: Parameterized queries
def get_user_good(cursor, user_id):
query = "SELECT * FROM users WHERE id = %s"
cursor.execute(query, (user_id,))
# Even better: Use ORM like SQLAlchemy
from sqlalchemy import create_engine, text
engine = create_engine('postgresql://user:password@localhost/db')
def get_user_safe(user_id):
with engine.connect() as conn:
result = conn.execute(
text("SELECT * FROM users WHERE id = :user_id"),
{"user_id": user_id}
)
return result.fetchone()
# 4. XSS prevention
from html import escape
def render_user_profile(user_data):
"""Render user profile safely."""
return f"""
{escape(user_data['name'])}
{escape(user_data['bio'])}
"""
# 5. CSRF protection
from flask import Flask, session, request
from werkzeug.security import generate_password_hash
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
@app.before_request
def csrf_protect():
"""CSRF protection middleware."""
if request.method == "POST":
token = session.get('_csrf_token')
if not token or token != request.form.get('_csrf_token'):
abort(403)
def generate_csrf_token():
"""Generate CSRF token."""
if '_csrf_token' not in session:
session['_csrf_token'] = secrets.token_hex(16)
return session['_csrf_token']
@app.route('/form', methods=['GET', 'POST'])
def protected_form():
if request.method == 'POST':
# Form processing
pass
return render_template('form.html', csrf_token=generate_csrf_token())
# 6. Secure file uploads
import os
from werkzeug.utils import secure_filename
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename):
"""Check if file extension is allowed."""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def handle_file_upload(file):
"""Handle file upload securely."""
if not file or not allowed_file(file.filename):
raise ValueError("Invalid file")
filename = secure_filename(file.filename)
# Check file size (max 10MB)
file.seek(0, os.SEEK_END)
size = file.tell()
file.seek(0)
if size > 10 * 1024 * 1024:
raise ValueError("File too large")
# Save file
file.save(os.path.join('uploads', filename))
return filename
# 7. Environment variable management
import os
from dotenv import load_dotenv
load_dotenv()
# Never hardcode secrets
DATABASE_URL = os.getenv('DATABASE_URL')
SECRET_KEY = os.getenv('SECRET_KEY')
API_KEY = os.getenv('API_KEY')
if not all([DATABASE_URL, SECRET_KEY, API_KEY]):
raise ValueError("Missing required environment variables")
# 8. Logging security
import logging
# Configure secure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
filename='app.log'
)
# Never log sensitive information
def authenticate_user(username, password):
# Bad: Logging password
logging.info(f"Authenticating user: {username} with password: {password}")
# Good: Log without sensitive data
logging.info(f"Authenticating user: {username}")
# 9. Rate limiting
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route("/login", methods=["POST"])
@limiter.limit("5 per minute")
def login():
# Login logic
pass
# 10. HTTPS enforcement
from flask_sslify import SSLify
app = Flask(__name__)
sslify = SSLify(app) # Redirect HTTP to HTTPS
# 11. Secure headers
from flask import make_response
@app.after_request
def add_security_headers(response):
"""Add security headers to response."""
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
response.headers['Content-Security-Policy'] = "default-src 'self'"
return response
# 12. Dependency security
# Keep dependencies updated
# Use tools like safety to check for vulnerabilities
# pip install safety
# safety check
# 13. Code review and security testing
# Use static analysis tools
# bandit - Python security linter
# pip install bandit
# bandit -r .
# 14. Data encryption
from cryptography.fernet import Fernet
def encrypt_data(data):
"""Encrypt sensitive data."""
key = Fernet.generate_key()
f = Fernet(key)
return f.encrypt(data.encode()), key
def decrypt_data(encrypted_data, key):
"""Decrypt data."""
f = Fernet(key)
return f.decrypt(encrypted_data).decode()
# 15. Secure random number generation
# Never use random module for security
import secrets
def generate_session_id():
"""Generate secure session ID."""
return secrets.token_hex(16)
def generate_password_reset_token():
"""Generate password reset token."""
return secrets.token_urlsafe(32)
Error Handling and Logging
Proper error handling and logging practices:
# Error handling best practices
# 1. Use specific exception types
class ValidationError(Exception):
"""Raised when validation fails."""
pass
class DatabaseError(Exception):
"""Raised when database operations fail."""
pass
class AuthenticationError(Exception):
"""Raised when authentication fails."""
pass
# 2. Proper exception handling
def process_user_data(user_data):
"""Process user data with proper error handling."""
try:
# Validate input
if not user_data.get('email'):
raise ValidationError("Email is required")
# Process data
user = create_user(user_data)
# Log success
logger.info(f"Successfully processed user: {user.id}")
return user
except ValidationError as e:
logger.warning(f"Validation failed: {e}")
raise # Re-raise validation errors
except DatabaseError as e:
logger.error(f"Database error: {e}")
# Attempt recovery or cleanup
rollback_transaction()
raise
except Exception as e:
logger.error(f"Unexpected error: {e}", exc_info=True)
# Don't expose internal errors to user
raise InternalServerError("An unexpected error occurred")
# 3. Context managers for resource management
class DatabaseConnection:
def __enter__(self):
self.connection = create_connection()
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
if self.connection:
self.connection.close()
def process_with_db():
with DatabaseConnection() as conn:
# Database operations
result = conn.execute("SELECT * FROM users")
return result.fetchall()
# Connection automatically closed
# 4. Logging configuration
import logging
import logging.config
LOGGING_CONFIG = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
},
'detailed': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'standard',
'level': 'INFO'
},
'file': {
'class': 'logging.FileHandler',
'filename': 'app.log',
'formatter': 'detailed',
'level': 'DEBUG'
},
'error_file': {
'class': 'logging.FileHandler',
'filename': 'error.log',
'formatter': 'detailed',
'level': 'ERROR'
}
},
'loggers': {
'': { # Root logger
'handlers': ['console', 'file', 'error_file'],
'level': 'DEBUG'
},
'requests': { # Third-party library logger
'handlers': ['file'],
'level': 'WARNING',
'propagate': False
}
}
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger(__name__)
# 5. Structured logging
import json
from datetime import datetime
class StructuredLogger:
def __init__(self, name):
self.logger = logging.getLogger(name)
def log_event(self, event_type, **data):
"""Log structured event data."""
log_entry = {
'timestamp': datetime.utcnow().isoformat(),
'event_type': event_type,
'data': data
}
self.logger.info(json.dumps(log_entry))
# Usage
structured_logger = StructuredLogger(__name__)
structured_logger.log_event('user_login', user_id=123, ip_address='192.168.1.1')
structured_logger.log_event('payment_processed', amount=99.99, currency='USD')
# 6. Custom exception hierarchy
class ApplicationError(Exception):
"""Base exception for application errors."""
def __init__(self, message, error_code=None):
super().__init__(message)
self.error_code = error_code or self.__class__.__name__
class BusinessLogicError(ApplicationError):
"""Errors related to business logic violations."""
pass
class ExternalServiceError(ApplicationError):
"""Errors from external service calls."""
pass
# 7. Error recovery and retry logic
import time
import random
def retry_with_backoff(func, max_attempts=3, backoff_factor=2):
"""Retry function with exponential backoff."""
for attempt in range(max_attempts):
try:
return func()
except Exception as e:
if attempt == max_attempts - 1:
raise
wait_time = backoff_factor ** attempt + random.uniform(0, 1)
logger.warning(f"Attempt {attempt + 1} failed: {e}. Retrying in {wait_time:.2f}s")
time.sleep(wait_time)
# Usage
@retry_with_backoff
def call_external_api():
# API call that might fail
pass
# 8. Graceful degradation
def get_user_preferences(user_id, default_preferences=None):
"""Get user preferences with fallback."""
try:
return load_user_preferences_from_db(user_id)
except DatabaseError:
logger.warning(f"Could not load preferences for user {user_id}, using defaults")
return default_preferences or {}
except Exception as e:
logger.error(f"Unexpected error loading preferences: {e}")
return default_preferences or {}
# 9. Error boundaries (similar to try-catch in other languages)
class ErrorBoundary:
def __init__(self, fallback_function=None):
self.fallback_function = fallback_function
def __call__(self, func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.error(f"Error in {func.__name__}: {e}", exc_info=True)
if self.fallback_function:
return self.fallback_function(*args, **kwargs)
raise
return wrapper
@ErrorBoundary(fallback_function=lambda: "Default response")
def risky_operation():
# Operation that might fail
pass
# 10. Monitoring and alerting
def send_alert(message, severity='error'):
"""Send alert to monitoring system."""
# Implementation depends on monitoring system
# Could send email, Slack message, etc.
logger.error(f"ALERT [{severity}]: {message}")
def monitor_function(func):
"""Decorator to monitor function performance and errors."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = func(*args, **kwargs)
duration = time.time() - start_time
logger.info(f"{func.__name__} completed in {duration:.2f}s")
return result
except Exception as e:
duration = time.time() - start_time
logger.error(f"{func.__name__} failed after {duration:.2f}s: {e}")
send_alert(f"Function {func.__name__} failed: {e}")
raise
return wrapper
Version Control and Collaboration
Best practices for version control and team collaboration:
# .gitignore for Python projects
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# Git commit message guidelines
# Format: type(scope): description
#
# Types:
# feat: A new feature
# fix: A bug fix
# docs: Documentation only changes
# style: Changes that do not affect the meaning of the code
# refactor: A code change that neither fixes a bug nor adds a feature
# test: Adding missing tests or correcting existing tests
# chore: Changes to the build process or auxiliary tools
#
# Examples:
# feat(auth): add user registration endpoint
# fix(api): handle null values in user data
# docs(readme): update installation instructions
# style: format code with black
# refactor(db): optimize query performance
# test: add unit tests for user service
# chore: update dependencies
# Branch naming conventions
# feature/feature-name
# bugfix/bug-description
# hotfix/critical-fix
# release/v1.2.3
# Pull request template
## Description
Brief description of the changes made.
## Type of Change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to break)
- [ ] Documentation update
## How Has This Been Tested?
Describe the tests that you ran to verify your changes.
## Checklist:
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
# Code review checklist
## Functionality
- [ ] Code compiles without errors
- [ ] Code runs without exceptions
- [ ] All tests pass
- [ ] New functionality works as expected
- [ ] Edge cases are handled properly
## Code Quality
- [ ] Code follows PEP 8 style guidelines
- [ ] Code is well-documented
- [ ] Variable and function names are descriptive
- [ ] No hardcoded values
- [ ] No unused imports or variables
## Security
- [ ] Input validation is implemented
- [ ] SQL injection prevention
- [ ] XSS prevention
- [ ] Authentication and authorization checks
- [ ] Sensitive data is not logged
## Performance
- [ ] No obvious performance issues
- [ ] Database queries are optimized
- [ ] Memory usage is reasonable
- [ ] Caching is implemented where appropriate
## Testing
- [ ] Unit tests are included
- [ ] Integration tests are included
- [ ] Edge cases are tested
- [ ] Error conditions are tested
## Documentation
- [ ] Code is documented
- [ ] README is updated if necessary
- [ ] API documentation is updated
- [ ] Comments explain complex logic
Final Thoughts
Following these best practices will help you write clean, maintainable, secure, and efficient Python code. Remember that best practices evolve over time, so stay updated with the latest developments in the Python community.