Python Pro
Guidelines for writing clean, performant, and idiomatic Python code.
Core Principles
- Pythonic code - Follow PEP 8, use Python idioms
- Composition over inheritance - Prefer mixins and protocols
- Explicit is better than implicit - Clear error handling
- Generators for efficiency - Lazy evaluation for large datasets
- Type hints everywhere - Enable static analysis with mypy
Code Patterns
Type Hints (Python 3.10+)
from typing import Protocol, TypeVar, Generic
from collections.abc import Callable, Iterator
T = TypeVar('T')
class Repository(Protocol[T]):
def get(self, id: str) -> T | None: ...
def save(self, item: T) -> None: ...
def process_items[T](items: list[T], fn: Callable[[T], T]) -> Iterator[T]:
for item in items:
yield fn(item)
Context Managers
from contextlib import contextmanager
from typing import Generator
@contextmanager
def managed_resource(name: str) -> Generator[Resource, None, None]:
resource = Resource(name)
try:
yield resource
finally:
resource.cleanup()
Decorators with Proper Typing
from functools import wraps
from typing import ParamSpec, TypeVar, Callable
P = ParamSpec('P')
R = TypeVar('R')
def retry(max_attempts: int = 3) -> Callable[[Callable[P, R]], Callable[P, R]]:
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
raise RuntimeError("Unreachable")
return wrapper
return decorator
Async Patterns
import asyncio
from typing import AsyncIterator
async def fetch_all[T](urls: list[str], parse: Callable[[str], T]) -> list[T]:
async with aiohttp.ClientSession() as session:
tasks = [fetch_one(session, url, parse) for url in urls]
return await asyncio.gather(*tasks)
async def stream_data() -> AsyncIterator[bytes]:
async with aiofiles.open('large.csv', 'rb') as f:
async for chunk in f:
yield chunk
Custom Exceptions
from dataclasses import dataclass
@dataclass
class ValidationError(Exception):
field: str
message: str
value: object = None
def __str__(self) -> str:
return f"{self.field}: {self.message} (got {self.value!r})"
Testing with Pytest
Fixtures and Parametrization
import pytest
from typing import Generator
@pytest.fixture
def db_session() -> Generator[Session, None, None]:
session = Session()
yield session
session.rollback()
@pytest.fixture
def sample_user(db_session: Session) -> User:
user = User(name="test", email="test@example.com")
db_session.add(user)
return user
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("World", "WORLD"),
("", ""),
])
def test_uppercase(input: str, expected: str) -> None:
assert input.upper() == expected
Async Testing
import pytest
@pytest.mark.asyncio
async def test_fetch_data() -> None:
result = await fetch_data("https://api.example.com")
assert result.status == "success"
Project Structure
project/
├── pyproject.toml # Modern Python config
├── src/
│ └── package/
│ ├── __init__.py
│ ├── py.typed # PEP 561 marker
│ ├── domain/ # Business logic
│ ├── services/ # Application services
│ └── adapters/ # External integrations
├── tests/
│ ├── conftest.py # Shared fixtures
│ ├── unit/
│ └── integration/
└── .python-version # pyenv version
pyproject.toml Template
[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []
[project.optional-dependencies]
dev = ["pytest>=8.0", "mypy>=1.8", "ruff>=0.2"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
[tool.mypy]
strict = true
python_version = "3.11"
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
Performance Tips
- Use
__slots__for data classes with many instances - Prefer
dict.get()overtry/except KeyError - Use
itertoolsfor efficient iteration - Profile with
cProfileandline_profiler - Use
functools.lru_cachefor expensive pure functions
Common Anti-Patterns to Avoid
- Mutable default arguments:
def f(items=[])→def f(items=None) - Bare
except:clauses → Always specify exception type - Using
type()for comparisons → Useisinstance() - String concatenation in loops → Use
"".join()or f-strings - Ignoring return values → Handle or explicitly discard with
_