| name | python-guide |
| description | Python guardrails, patterns, and best practices for AI-assisted development.
Use when working with Python files (.py), requirements.txt, pyproject.toml,
or when the user mentions Python. Provides type hint conventions, virtual environment
standards, testing with pytest, and async patterns.
|
| license | MIT |
| metadata | {"author":"samuel","version":"1.0","category":"language","language":"python","extensions":".py"} |
Python Guide
Applies to: Python 3.11+, APIs, CLIs, Data Pipelines, Automation
Core Principles
- Type Hints Everywhere: All function signatures, class attributes, and module-level variables must have type annotations
- Explicit Over Implicit: No
* imports, no mutable default arguments, no implicit type coercions
- Virtual Environments Always: Never install into system Python; use
venv, uv, or poetry
- Pytest Over unittest: Use pytest for all testing; fixtures and parametrize over setUp/tearDown
- PEP 8 + Ruff: Enforce style mechanically; never rely on manual formatting
Guardrails
Python Version
- Target Python 3.11+ (use
match statements, ExceptionGroup, tomllib)
- Set
requires-python = ">=3.11" in pyproject.toml
- Use
from __future__ import annotations for forward references in 3.11
- Never use features removed in 3.12+ (
distutils, imp, legacy typing aliases)
Code Style
- Run
ruff check and ruff format before every commit
- Max line length: 88 characters (Black default)
- Imports: stdlib, blank line, third-party, blank line, local (enforced by
isort/ruff)
- Naming:
snake_case for functions/variables, PascalCase for classes, UPPER_SNAKE for constants
- No bare
except: โ always catch specific exceptions
- No mutable default arguments (
def f(items=None): not def f(items=[]):)
- Prefer f-strings over
.format() or % formatting
- Use
pathlib.Path instead of os.path for all file operations
Type Hints
- All public functions MUST have full type annotations (params + return)
- Use
collections.abc types: Sequence, Mapping, Iterable (not List, Dict)
- Use
X | None union syntax (not Optional[X])
- Use
TypeAlias for complex types: UserMap: TypeAlias = dict[str, User]
- Use
Protocol for structural subtyping (duck typing with safety)
- Use
@overload for functions returning different types based on input
- Run
mypy --strict in CI (no type: ignore without explanation)
from collections.abc import Sequence
def find_users(
ids: Sequence[str],
*,
active_only: bool = True,
) -> list[User]:
"""Fetch users by ID list, optionally filtering inactive."""
...
Error Handling
- Never use bare
except: or except Exception: without re-raising
- Create domain-specific exception hierarchies rooted in a base class
- Use
raise ... from err to preserve exception chains
- Log at the boundary, raise in the interior (don't log-and-raise)
- Use
contextlib.suppress() instead of empty except blocks
- Always close resources with
with statements or contextlib.closing
Dependencies
- Define all deps in
pyproject.toml (not setup.py or bare requirements.txt)
- Pin exact versions in lock files (
uv.lock, poetry.lock, pip-compile output)
- Keep
requirements.txt only as a generated artifact, never hand-edited
- Separate
[project.optional-dependencies] for dev, test, docs
- Audit with
pip-audit or safety before adding new packages
- Prefer stdlib solutions:
tomllib, pathlib, dataclasses, enum, logging
Project Structure
myproject/
โโโ src/
โ โโโ myproject/ # Importable package (src layout)
โ โโโ __init__.py
โ โโโ py.typed # PEP 561 marker for type stubs
โ โโโ domain/ # Business logic, entities
โ โ โโโ __init__.py
โ โ โโโ models.py
โ โ โโโ exceptions.py
โ โโโ service/ # Application services
โ โ โโโ __init__.py
โ โโโ repository/ # Data access layer
โ โ โโโ __init__.py
โ โโโ api/ # HTTP/CLI interface
โ โโโ __init__.py
โโโ tests/
โ โโโ conftest.py # Shared fixtures
โ โโโ unit/
โ โโโ integration/
โโโ pyproject.toml # Single source of truth for config
โโโ uv.lock # Or poetry.lock
โโโ README.md
- Use src layout (
src/myproject/) to prevent accidental local imports
- Keep
conftest.py at test root for shared fixtures; nest for scope
- Include
py.typed marker for downstream type checking
- No
__init__.py in tests/ (pytest discovers without it)
- One module = one responsibility; split at ~200 lines
Error Handling Patterns
Exception Hierarchy
class AppError(Exception):
"""Base exception for the application."""
def __init__(self, message: str, *, code: str = "UNKNOWN") -> None:
super().__init__(message)
self.code = code
class NotFoundError(AppError):
"""Raised when a requested resource does not exist."""
def __init__(self, resource: str, identifier: str) -> None:
super().__init__(
f"{resource} with id '{identifier}' not found",
code="NOT_FOUND",
)
self.resource = resource
self.identifier = identifier
class ValidationError(AppError):
"""Raised when input data fails validation."""
def __init__(self, field: str, reason: str) -> None:
super().__init__(
f"Validation failed for '{field}': {reason}",
code="VALIDATION_ERROR",
)
Context Managers for Cleanup
from contextlib import contextmanager
from collections.abc import Generator
@contextmanager
def managed_connection(url: str) -> Generator[Connection, None, None]:
conn = Connection(url)
try:
conn.open()
yield conn
except ConnectionError as err:
raise AppError("Database unavailable") from err
finally:
conn.close()
Error Chaining
def get_user(user_id: str) -> User:
try:
row = db.fetch_one("SELECT * FROM users WHERE id = %s", (user_id,))
except DatabaseError as err:
raise AppError(f"Failed to fetch user {user_id}") from err
if row is None:
raise NotFoundError("User", user_id)
return User.from_row(row)
Testing
Standards
- Test files:
test_*.py (same name as module: models.py -> test_models.py)
- Test functions:
test_<unit>_<scenario>_<expected> (e.g., test_get_user_not_found_raises)
- Use
conftest.py for fixtures shared across a directory
- Coverage target: >80% for business logic, >60% overall
- Mark slow tests:
@pytest.mark.slow and exclude from default runs
- No
unittest.TestCase โ use plain functions with pytest assertions
- Use
tmp_path fixture for file operations (auto-cleanup)
Fixtures and Parametrize
import pytest
from myproject.domain.models import User
@pytest.fixture
def sample_user() -> User:
return User(id="u-123", name="Ada Lovelace", email="ada@example.com")
@pytest.mark.parametrize(
("email", "is_valid"),
[
("user@example.com", True),
("user@.com", False),
("", False),
("user@domain", False),
],
)
def test_validate_email(email: str, is_valid: bool) -> None:
assert validate_email(email) == is_valid
def test_get_user_returns_user(sample_user: User) -> None:
repo = InMemoryUserRepo(users=[sample_user])
result = repo.get("u-123")
assert result == sample_user
def test_get_user_not_found_raises() -> None:
repo = InMemoryUserRepo(users=[])
with pytest.raises(NotFoundError, match="User.*not found"):
repo.get("nonexistent")
Mocking External Dependencies
from unittest.mock import AsyncMock, patch
async def test_send_notification_retries_on_failure() -> None:
mock_client = AsyncMock()
mock_client.post.side_effect = [ConnectionError, None]
with patch("myproject.service.notify.http_client", mock_client):
await send_notification(user_id="u-123", message="hello")
assert mock_client.post.call_count == 2
Tooling
pyproject.toml Configuration
[project]
name = "myproject"
requires-python = ">=3.11"
[project.optional-dependencies]
dev = ["ruff", "mypy", "pytest", "pytest-cov", "pytest-asyncio"]
[tool.ruff]
target-version = "py311"
line-length = 88
[tool.ruff.lint]
select = [
"E",
"W",
"F",
"I",
"N",
"UP",
"B",
"S",
"A",
"C4",
"SIM",
"RUF",
]
[tool.mypy]
strict = true
warn_return_any = true
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = []
= []
=
= []
=
=
=
= [, ]
Essential Commands
ruff check .
ruff format .
mypy .
pytest
pytest --cov=src -q
pytest -m "not slow"
pip-audit
python -m build
Advanced Topics
For detailed patterns and examples, see:
External References