Python Best Practices
Modern Python development practices and patterns
Python Best Practices
Write clean, maintainable, and efficient Python code following industry best practices.
Code Style
PEP 8 Compliance
Follow the Python Enhancement Proposal 8 style guide:
# Good: Clear variable names, proper spacing
def calculate_total_price(items, tax_rate=0.08):
"""Calculate total price including tax."""
subtotal = sum(item.price for item in items)
tax = subtotal * tax_rate
return subtotal + tax
# Bad: Poor naming, inconsistent spacing
def calc(i,t=0.08):
s=sum(x.p for x in i)
return s+s*tNaming Conventions
# Classes: PascalCase
class UserAccount:
pass
# Functions and variables: snake_case
def get_user_data():
user_name = "John"
# Constants: UPPER_CASE
MAX_CONNECTIONS = 100
API_TIMEOUT = 30
# Private attributes: leading underscore
class Database:
def __init__(self):
self._connection = NoneType Hints
Use type hints for better code documentation and IDE support:
from typing import List, Dict, Optional, Union
def process_users(
users: List[Dict[str, str]],
active_only: bool = True
) -> List[str]:
"""Process user data and return list of names."""
return [
user['name']
for user in users
if not active_only or user.get('active', False)
]
def find_user(user_id: int) -> Optional[Dict[str, str]]:
"""Find user by ID, returns None if not found."""
# Implementation here
passError Handling
Specific Exceptions
# Good: Catch specific exceptions
try:
file_content = open('data.txt').read()
data = json.loads(file_content)
except FileNotFoundError:
logger.error("Data file not found")
data = {}
except json.JSONDecodeError:
logger.error("Invalid JSON format")
data = {}
# Bad: Bare except catches everything
try:
# ... code ...
except:
pass # Silent failure, hard to debugContext Managers
# Good: Automatic resource cleanup
with open('data.txt', 'r') as f:
content = f.read()
# File is automatically closed
# For custom resources
from contextlib import contextmanager
@contextmanager
def database_connection(db_url):
conn = create_connection(db_url)
try:
yield conn
finally:
conn.close()
with database_connection('postgresql://...') as conn:
conn.execute('SELECT * FROM users')List Comprehensions
# Good: Readable comprehensions
active_users = [
user for user in users
if user.is_active
]
# Use generator expressions for large datasets
total = sum(
item.price
for item in large_list
if item.in_stock
)
# Bad: Too complex
result = [x.upper() if len(x) > 3 else x.lower()
for x in items if x is not None and
validate(x) and x not in exclude_list]
# Consider using a regular loop insteadFunctions
Single Responsibility
# Good: Each function does one thing
def fetch_user_data(user_id: int) -> Dict:
"""Fetch user data from API."""
response = requests.get(f'/api/users/{user_id}')
return response.json()
def validate_user_data(data: Dict) -> bool:
"""Validate user data structure."""
required_fields = ['id', 'name', 'email']
return all(field in data for field in required_fields)
def process_user(user_id: int) -> Dict:
"""Main function that orchestrates the process."""
data = fetch_user_data(user_id)
if validate_user_data(data):
return data
raise ValueError("Invalid user data")
# Bad: Function does too many things
def process_user_bad(user_id):
response = requests.get(f'/api/users/{user_id}')
data = response.json()
if 'id' not in data or 'name' not in data:
raise ValueError("Invalid")
# ... more processing ...
# ... even more processing ...
return dataDefault Arguments
# Good: Immutable defaults
def add_item(item: str, items: Optional[List[str]] = None) -> List[str]:
if items is None:
items = []
items.append(item)
return items
# Bad: Mutable default (shared between calls!)
def add_item_bad(item: str, items: List[str] = []) -> List[str]:
items.append(item)
return itemsClasses
Property Decorators
class User:
def __init__(self, first_name: str, last_name: str):
self._first_name = first_name
self._last_name = last_name
self._email = None
@property
def full_name(self) -> str:
"""Full name computed from first and last name."""
return f"{self._first_name} {self._last_name}"
@property
def email(self) -> Optional[str]:
return self._email
@email.setter
def email(self, value: str):
if '@' not in value:
raise ValueError("Invalid email")
self._email = value
# Usage
user = User("John", "Doe")
print(user.full_name) # "John Doe"
user.email = "john@example.com"Dataclasses
from dataclasses import dataclass, field
from typing import List
@dataclass
class Product:
name: str
price: float
description: str = ""
tags: List[str] = field(default_factory=list)
def __post_init__(self):
"""Validate after initialization."""
if self.price < 0:
raise ValueError("Price cannot be negative")
# Usage
product = Product(
name="Widget",
price=29.99,
tags=["electronics", "gadget"]
)Testing
Unit Tests with pytest
import pytest
from myapp import calculate_total_price, Item
def test_calculate_total_price_basic():
"""Test basic price calculation."""
items = [Item(price=10), Item(price=20)]
total = calculate_total_price(items)
assert total == 32.4 # 30 + 8% tax
def test_calculate_total_price_custom_tax():
"""Test with custom tax rate."""
items = [Item(price=100)]
total = calculate_total_price(items, tax_rate=0.10)
assert total == 110
@pytest.fixture
def sample_items():
"""Reusable test fixture."""
return [Item(price=10), Item(price=20), Item(price=30)]
def test_with_fixture(sample_items):
"""Test using fixture."""
total = calculate_total_price(sample_items)
assert total > 0Mocking
from unittest.mock import Mock, patch
def test_api_call():
"""Test function that makes API calls."""
with patch('requests.get') as mock_get:
mock_get.return_value.json.return_value = {
'id': 1,
'name': 'Test User'
}
result = fetch_user_data(1)
assert result['name'] == 'Test User'
mock_get.assert_called_once_with('/api/users/1')Performance
Use Built-in Functions
# Good: Use built-in functions (written in C)
numbers = [1, 2, 3, 4, 5]
total = sum(numbers)
maximum = max(numbers)
# Bad: Manual implementation
total = 0
for n in numbers:
total += nList vs Generator
# For large datasets, use generators
def read_large_file(file_path):
"""Generator for reading large files."""
with open(file_path) as f:
for line in f:
yield line.strip()
# Use the generator
for line in read_large_file('huge_file.txt'):
process(line) # Memory efficient
# Bad: Loading everything into memory
lines = [line.strip() for line in open('huge_file.txt')]Caching
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
"""Cached Fibonacci calculation."""
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Database query caching
from functools import cache
@cache
def get_expensive_data():
"""Cache expensive operations."""
return database.query().all()Project Structure
myproject/
├── src/
│ └── myproject/
│ ├── __init__.py
│ ├── main.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── services/
│ │ ├── __init__.py
│ │ └── api.py
│ └── utils/
│ ├── __init__.py
│ └── helpers.py
├── tests/
│ ├── __init__.py
│ ├── test_models.py
│ └── test_services.py
├── docs/
├── requirements.txt
├── setup.py
├── README.md
└── .gitignoreDependencies
requirements.txt
# Production dependencies
requests==2.31.0
pydantic==2.5.0
fastapi==0.104.1
# Development dependencies (requirements-dev.txt)
pytest==7.4.3
black==23.11.0
mypy==1.7.1
pylint==3.0.2Using pyproject.toml
[tool.poetry]
name = "myproject"
version = "0.1.0"
description = "My awesome project"
[tool.poetry.dependencies]
python = "^3.9"
requests = "^2.31.0"
[tool.poetry.dev-dependencies]
pytest = "^7.4.3"
black = "^23.11.0"
[tool.black]
line-length = 88
target-version = ['py39']
[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = trueTools
Code Formatting
# Black (opinionated formatter)
black src/
# isort (import sorting)
isort src/Linting
# Pylint
pylint src/
# Flake8
flake8 src/
# mypy (type checking)
mypy src/Pre-commit Hooks
repos:
- repo: https://github.com/psf/black
rev: 23.11.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
hooks:
- id: flake8Documentation
def complex_function(
param1: str,
param2: int,
optional_param: Optional[bool] = None
) -> Dict[str, any]:
"""
Brief one-line description.
More detailed explanation of what the function does,
including any important details about the implementation.
Args:
param1: Description of first parameter
param2: Description of second parameter
optional_param: Description of optional parameter
Returns:
Dictionary containing the results with keys:
- 'status': Operation status
- 'data': Processed data
Raises:
ValueError: If param1 is empty
TypeError: If param2 is not an integer
Example:
>>> result = complex_function("test", 42)
>>> print(result['status'])
'success'
"""
if not param1:
raise ValueError("param1 cannot be empty")
# Implementation...
return {'status': 'success', 'data': {}}