Python

Python Best Practices for 2024

Python Best Practices for 2024

Python continues to evolve, and so do the best practices for writing clean, maintainable code. This guide covers the most important patterns and practices that modern Python developers should know.

1. Type Hints Are Non-Negotiable

Type hints make your code self-documenting, enable better IDE support, and catch bugs before runtime. With Python 3.10+, the syntax is cleaner than ever:

python
from typing import Optional

# Basic function signatures
def greet(name: str, times: int = 1) -> str:
    return (f"Hello, {name}! " * times).strip()

# Modern union syntax (Python 3.10+)
def process(data: list[dict] | None) -> dict[str, int]:
    if data is None:
        return {}
    return {item["key"]: item["value"] for item in data}

# Generic collections (no need for typing.List, typing.Dict)
def get_users() -> list[dict[str, str]]:
    return [{"name": "Alice", "email": "alice@example.com"}]

# Optional is still useful for clarity
def find_user(user_id: int) -> Optional[User]:
    return User.objects.filter(id=user_id).first()
Run mypy in your CI pipeline to catch type errors before they reach production. Start with mypy --strict on new projects.

2. Embrace Dataclasses

Dataclasses eliminate boilerplate for data containers. Use them instead of plain classes or named tuples:

python
from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class User:
    id: int
    name: str
    email: str
    created_at: datetime = field(default_factory=datetime.now)
    roles: list[str] = field(default_factory=list)

    def display_name(self) -> str:
        return self.name.title()

# Immutable dataclass (recommended for value objects)
@dataclass(frozen=True)
class Point:
    x: float
    y: float

    def distance_from_origin(self) -> float:
        return (self.x ** 2 + self.y ** 2) ** 0.5

# Usage
user = User(id=1, name="alice", email="alice@example.com")
print(user)  # User(id=1, name='alice', email='alice@example.com', ...)
Dataclass options explained
frozen=True makes the dataclass immutable (hashable, safer in concurrent code)

field(default_factory=...) is required for mutable default values like lists

slots=True (Python 3.10+) reduces memory usage and speeds up attribute access

kw_only=True (Python 3.10+) requires all fields to be passed as keyword arguments

3. Context Managers for Resource Management

Context managers ensure resources are properly cleaned up, even when exceptions occur:

python
from contextlib import contextmanager
import time

@contextmanager
def timer(label: str):
    """Context manager to measure execution time."""
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed:.3f}s")

# Usage
with timer("Database query"):
    results = db.execute(query)

# Also great for temporary state changes
@contextmanager
def temporary_env(key: str, value: str):
    """Temporarily set an environment variable."""
    import os
    original = os.environ.get(key)
    os.environ[key] = value
    try:
        yield
    finally:
        if original is None:
            del os.environ[key]
        else:
            os.environ[key] = original

4. Use Pathlib for File Operations

pathlib provides an object-oriented interface to filesystem paths. It's more readable and less error-prone than os.path:

python
from pathlib import Path

# Create paths (works on all platforms)
config_dir = Path.home() / ".config" / "myapp"
config_file = config_dir / "settings.json"

# Check existence and type
if config_file.exists() and config_file.is_file():
    content = config_file.read_text()

# Create directories (parents=True is like mkdir -p)
config_dir.mkdir(parents=True, exist_ok=True)

# Glob patterns
for py_file in Path("src").glob("**/*.py"):
    print(py_file.name)

# File operations
config_file.write_text('{"debug": true}')
data = config_file.read_bytes()

# Path manipulation
print(config_file.stem)      # "settings"
print(config_file.suffix)    # ".json"
print(config_file.parent)    # PosixPath('/home/user/.config/myapp')

5. Prefer Composition Over Inheritance

Deep inheritance hierarchies are hard to understand and maintain. Favor composition and dependency injection:

python
# Instead of inheritance...
class EmailNotifier(BaseNotifier):
    def notify(self, message): ...

class SlackNotifier(BaseNotifier):
    def notify(self, message): ...

# ...use composition with protocols
from typing import Protocol

class Notifier(Protocol):
    def notify(self, message: str) -> None: ...

class EmailNotifier:
    def notify(self, message: str) -> None:
        send_email(message)

class SlackNotifier:
    def notify(self, message: str) -> None:
        post_to_slack(message)

class AlertService:
    def __init__(self, notifiers: list[Notifier]):
        self.notifiers = notifiers

    def alert(self, message: str) -> None:
        for notifier in self.notifiers:
            notifier.notify(message)

# Easy to test, easy to extend
service = AlertService([EmailNotifier(), SlackNotifier()])

6. Async When It Makes Sense

Python's async/await is powerful for I/O-bound operations, but don't use it everywhere:

python
import asyncio
import httpx

async def fetch_all(urls: list[str]) -> list[dict]:
    """Fetch multiple URLs concurrently."""
    async with httpx.AsyncClient() as client:
        tasks = [client.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return [r.json() for r in responses]

# Use asyncio.run() to run async code from sync context
results = asyncio.run(fetch_all([
    "https://api.example.com/users",
    "https://api.example.com/posts",
    "https://api.example.com/comments",
]))
Use async for I/O-bound operations (HTTP requests, database queries, file I/O). For CPU-bound work, use multiprocessing instead.

Summary

  1. Always use type hints for function signatures and class attributes
  2. Use dataclasses for structured data containers
  3. Leverage context managers for resource cleanup
  4. Use pathlib for all filesystem operations
  5. Prefer composition and protocols over deep inheritance
  6. Use async/await for I/O-bound concurrent operations
  7. Write small, focused functions that do one thing well
  8. Use f-strings for string formatting

Related Posts

Comments

Log in to leave a comment.

Log In

Loading comments...