Python Best Practices
Description
Writes idiomatic, well-documented Python code that follows PEP 8, uses type hints, manages virtual environments and packaging, and is ready for testing.
When to Use
give me Python best practices | show me how to organize a Python project | how to write type hints and tests in Python | refactor this script to idiomatic Python
Use Cases
Convert a script to idiomatic Python with tests. | Set up a new Python project with venv and pyproject.toml. | Add type hints to public APIs for clarity. | Configure Black and Ruff for formatting and linting. | Refactor code to use dataclasses and context managers.
SKILL.md Content
---
name: python-best-practices
description: "Writes idiomatic, well-documented Python code that follows PEP 8, uses type hints, manages virtual environments and packaging, and is ready for testing."
metadata:
tags: "autonomous-agents, python, python-best-practices, pep8, type-hints, virtual-environments, testing"
source: "https://skilldb.dev/skills/autonomous-agent-skills/python-best-practices"
pack: "autonomous-agent-skills"
category: "Autonomous Agents"
---
# Python Best Practices
## When to use this skill
Use when the user says things like:
- "give me Python best practices"
- "show me how to organize a Python project"
- "how to write type hints and tests in Python"
- "refactor this script to idiomatic Python"
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.