Python Best Practices
February 28, 2025 32174b4 Edit this page
⚠️ Caution: Update Needed
262 day(s) old

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*t

Naming 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 = None

Type 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
    pass

Error 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 debug

Context 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 instead

Functions

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 data

Default 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 items

Classes

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 > 0

Mocking

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 += n

List 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
└── .gitignore

Dependencies

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.2

Using 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 = true

Tools

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: flake8

Documentation

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': {}}

Resources