Best Practices
PEP 8, type hints, virtual environments, packaging, common anti-patterns
Writing Pythonic code means more than correct syntax — it means following community conventions (PEP 8), using type hints for correctness and tooling, managing environments properly, and packaging code for distribution. These practices make Python code readable, maintainable, and production-ready.
Key Points
- PEP 8: 4-space indentation, 79-char line limit (99 for code, 72 for docstrings), snake_case for functions/vars, PascalCase for classes
- Type hints (PEP 484): def greet(name: str) -> str — not enforced at runtime but enables mypy/pyright static checking
- Use from __future__ import annotations (Python 3.7+) or Python 3.10+ union type X | Y instead of Optional[X] / Union[X,Y]
- Virtual environments: python -m venv .venv — always isolate project dependencies; pyenv for multiple Python versions
- pyproject.toml: modern packaging standard (PEP 518/621) replacing setup.py — use with uv, Poetry, or Hatch
- Linting: ruff (replaces flake8 + isort + pyupgrade — 100x faster); formatting: black (deterministic)
- Avoid mutable default arguments: def f(items=[]) is a bug — the list persists across calls; use def f(items=None): items = items or []
- Use __all__ to define the public API of a module — controls what from module import * exports
- Prefer pathlib.Path over os.path for file operations — object-oriented, cross-platform, composable with /operator
| Anti-pattern | Problem | Pythonic fix |
|---|---|---|
| def f(items=[]) | Mutable default shared across calls | def f(items=None): items = items or [] |
| except Exception: pass | Silences all errors | Log or re-raise; be specific about exception type |
| import * | Pollutes namespace, unclear source | Explicit imports |
| type(x) == list | Breaks for subclasses | isinstance(x, list) |
| if x == None | Doesn't work for __eq__ override | if x is None |
| for i in range(len(lst)): lst[i] | Unidiomatic | for item in lst or enumerate(lst) |
| string += in loop | O(n²) for n concatenations | "".join(parts) |
Pythonic best practices: type hints with Protocol, pathlib, __slots__, pyproject.toml config
# Type hints — modern style (Python 3.10+)
from pathlib import Path
from typing import Protocol
def read_config(path: Path) -> dict[str, str]:
return dict(line.split("=", 1) for line in path.read_text().splitlines() if "=" in line)
# Protocol — structural typing (duck typing with type safety)
class Drawable(Protocol):
def draw(self) -> None: ...
def render_all(shapes: list[Drawable]) -> None:
for shape in shapes: shape.draw()
# __all__ — public API
__all__ = ["MyClass", "helper_function"]
# pathlib — composable file paths
base = Path("/data")
config = base / "config" / "settings.toml" # cross-platform
if config.exists():
text = config.read_text(encoding="utf-8")
# dataclass + __slots__ — memory-efficient
from dataclasses import dataclass
@dataclass
class Point:
__slots__ = ("x", "y")
x: float
y: float
# pyproject.toml snippet
# [project]
# name = "mypackage"
# version = "1.0.0"
# requires-python = ">=3.11"
# dependencies = ["pydantic>=2.0", "httpx"]
#
# [tool.ruff.lint]
# select = ["E", "W", "F", "I"]
#
# [tool.mypy]
# strict = trueReal-World Example
mypy --strict on a large codebase catches real bugs: missing None checks (Optional not handled), wrong argument types, unreachable code. Teams that add type hints incrementally using # type: ignore sparingly report 30-50% fewer production type errors. ruff replaces flake8 + isort + dozens of plugins and runs in milliseconds — no reason not to add it to every project CI.