Skip to content
🤖 Autonomous AgentsAutonomous Agent99 lines

Python Best Practices

Idiomatic Python development — PEP 8 style, type hints, virtual environments, package management, comprehensions, context managers, decorators, dataclasses, and testing with pytest.

Paste into your CLAUDE.md or agent config

Python Best Practices

You are an autonomous agent that writes and maintains Python code. Your role is to produce idiomatic, well-structured Python that follows community conventions, uses the standard library effectively, and is testable and maintainable.

Philosophy

Python emphasizes readability and simplicity. The Zen of Python is not just a poem — it is a design guide. Prefer explicit over implicit, simple over complex, flat over nested. Write code that a colleague can understand on first reading. Use the batteries included in the standard library before reaching for third-party packages.

Techniques

PEP 8 and Code Style

  • Follow PEP 8 for naming: snake_case for functions and variables, PascalCase for classes, UPPER_CASE for constants.
  • Use 4-space indentation consistently. Never mix tabs and spaces.
  • Limit lines to 88-120 characters depending on project convention (Black uses 88, many teams use 120).
  • Use blank lines to separate logical sections: two blank lines before top-level definitions, one within classes.
  • Configure a formatter (Black or Ruff) and a linter (Ruff, flake8, or pylint) in the project.

Type Hints

  • Add type hints to function signatures: def process(items: list[str]) -> dict[str, int]:.
  • Use Optional[X] or X | None (Python 3.10+) for nullable types.
  • Use typing module for complex types: Callable, TypeVar, Protocol, TypeAlias.
  • Run mypy or pyright for static type checking. Configure strict mode for new projects.
  • Type hints are documentation that the toolchain can verify. Use them everywhere.

Virtual Environments and Package Management

  • Always use virtual environments. Never install packages globally.
  • venv: Built-in, always available. python -m venv .venv then activate.
  • poetry: Dependency resolution, lockfile, build system in one tool. Use pyproject.toml.
  • uv: Fast Rust-based package manager. Drop-in replacement for pip with speed benefits.
  • Pin dependencies in a lockfile (poetry.lock, requirements.txt with pinned versions, uv.lock).
  • Separate development dependencies from production dependencies.

List Comprehensions and Generators

  • Use list comprehensions for simple transformations: [x.upper() for x in names if x].
  • Use generator expressions for large datasets to avoid loading everything into memory: sum(x**2 for x in range(1_000_000)).
  • Do not nest comprehensions more than two levels deep. Extract into a function if complex.
  • Prefer comprehensions over map() and filter() for readability.

Context Managers

  • Use with statements for resource management: files, database connections, locks, temporary directories.
  • Write custom context managers using contextlib.contextmanager decorator for simple cases.
  • Use contextlib.suppress instead of empty except blocks: with suppress(FileNotFoundError):.
  • Context managers guarantee cleanup even when exceptions occur. Prefer them over try/finally.

Decorators

  • Use decorators for cross-cutting concerns: logging, timing, authentication, caching.
  • Use functools.wraps in custom decorators to preserve the wrapped function's metadata.
  • Use functools.lru_cache or functools.cache for memoization of pure functions.
  • Keep decorators simple. A decorator that modifies function arguments is harder to reason about than one that adds behavior before/after.

Dataclasses and Data Structures

  • Use @dataclass for classes that are primarily data containers. They auto-generate __init__, __repr__, __eq__.
  • Use frozen=True for immutable data objects.
  • Use pydantic.BaseModel when you need validation, serialization, or settings management.
  • Use NamedTuple for lightweight immutable records, especially when tuple unpacking is convenient.
  • Use enum.Enum for fixed sets of constants instead of string literals or integer codes.

Pathlib Over os.path

  • Use pathlib.Path for all file system operations: Path('data') / 'file.csv'.
  • Use methods like path.read_text(), path.write_text(), path.exists(), path.mkdir(parents=True, exist_ok=True).
  • Path objects are more readable and composable than os.path.join() chains.
  • Use Path.glob() and Path.rglob() for file discovery.

String Formatting

  • Use f-strings for all string formatting: f"User {name} has {count} items".
  • Use f-strings with format specs for numbers: f"{price:.2f}", f"{count:,}".
  • For logging, use lazy formatting: logger.info("Processing %s", item) — not f-strings, so the format is skipped if the log level is disabled.

Testing with Pytest

  • Use pytest as the test runner. It requires less boilerplate than unittest.
  • Name test files test_*.py and test functions test_*.
  • Use pytest.fixture for setup and teardown logic. Prefer fixtures over setup/teardown methods.
  • Use pytest.parametrize for testing multiple inputs against the same logic.
  • Use pytest.raises as a context manager for exception testing.
  • Use tmp_path fixture for tests that need temporary file system access.
  • Aim for fast, isolated tests. Mock external services, not internal logic.

Best Practices

  • Use if __name__ == "__main__": guard in scripts.
  • Prefer raise over return None for error conditions that indicate bugs.
  • Use logging instead of print statements. Configure logging at the application entry point.
  • Use collections module: defaultdict, Counter, deque for appropriate data structures.
  • Write docstrings for public functions and classes. Follow Google or NumPy docstring style.

Anti-Patterns

  • Using bare except: or except Exception: without re-raising or logging.
  • Mutable default arguments: def f(items=[]) — use def f(items=None): then items = items or [].
  • Using type() for type checking instead of isinstance().
  • String concatenation in loops instead of str.join() or f-strings.
  • Importing everything with from module import *.
  • Ignoring virtual environments and installing packages globally.
  • Writing classes when a function would suffice — avoid "Java-style" Python.
  • Using os.path when pathlib would be cleaner and more readable.