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:
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()
mypy --strict on new projects.
2. Embrace Dataclasses
Dataclasses eliminate boilerplate for data containers. Use them instead of plain classes or named tuples:
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
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:
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:
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:
# 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:
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",
]))
Summary
- Always use type hints for function signatures and class attributes
- Use dataclasses for structured data containers
- Leverage context managers for resource cleanup
- Use pathlib for all filesystem operations
- Prefer composition and protocols over deep inheritance
- Use async/await for I/O-bound concurrent operations
- Write small, focused functions that do one thing well
- Use f-strings for string formatting
Related Posts
Building Interactive UIs with Django and HTMX
Learn how to create dynamic, JavaScript-free interfaces using Django and HTMX. Build modern web apps without the complexity of SPAs.
1 min read
Introduction to Machine Learning with Python
Start your ML journey with Python and scikit-learn. Build your first machine learning models with practical examples.
1 min read
10 Tailwind CSS Tips You Need to Know
Level up your Tailwind CSS skills with these 10 practical tips. From responsive design to custom configurations, become a Tailwind power user.
1 min read
Redis Caching Patterns for Web Applications
Speed up your web application with Redis caching. Learn cache-aside, write-through, and other patterns with practical examples.
1 min read
Comments
Log in to leave a comment.
Log In
Loading comments...