From python-foundation
Modern Python idioms for any Python 3.10+ project: PEP 8 style, PEP 484/526 type hints, dataclasses, pathlib, enums, f-strings, structural pattern matching (match/case), context managers, exception handling, and null discipline. Stack-agnostic — referenced by every Python plugin in the marketplace. Use this skill to: - Write strongly-typed code with type hints on every function signature and class attribute. - Replace isinstance chains and magic constants with match/case and enums. - Use dataclasses or NamedTuple for value objects and data containers. - Handle resources safely with context managers and pathlib for all file paths. - Apply PEP 8 naming and structure conventions consistently. Do NOT use this skill for: - Framework-specific idioms (Django ORM, FastAPI routers, Flask blueprints — those live in framework plugin skills). - Packaging and dependency management — see python-foundation:python-tooling. - Testing patterns — see python-foundation:pytest-testing.
How this skill is triggered — by the user, by Claude, or both
Slash command
/python-foundation:python-conventionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill encodes idioms that reduce bugs and improve readability in any Python codebase. Apply alongside the active framework plugin's conventions skill (e.g., `django-plugin:django-conventions`, `fastapi-plugin:fastapi-conventions`).
This skill encodes idioms that reduce bugs and improve readability in any Python codebase. Apply alongside the active framework plugin's conventions skill (e.g., django-plugin:django-conventions, fastapi-plugin:fastapi-conventions).
Read pyproject.toml to determine the target Python version before writing code:
# pyproject.toml — Poetry style
[tool.poetry.dependencies]
python = "^3.10"
# pyproject.toml — PEP 621 style
[project]
requires-python = ">=3.10"
Also check for a .python-version file (used by pyenv):
cat .python-version # e.g. "3.12.3"
Match the project's minimum version — do not use Python 3.11+ features (e.g. StrEnum, tomllib, ExceptionGroup) on a 3.10 project. All idioms in this skill apply from 3.10 onwards.
Annotate every function parameter, return type, and class attribute. Type hints are documentation that a machine can check.
from __future__ import annotations # enables forward references on 3.10
from collections.abc import Sequence
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}!\n" * times).rstrip()
def first_active(users: Sequence[User]) -> User | None: # union syntax (3.10+)
return next((u for u in users if u.is_active), None)
Use from __future__ import annotations at the top of files that reference types not yet defined at parse time (forward references, self-referential types). It defers evaluation of all annotations to strings, avoiding NameError.
For library or shared utility code, use TypeVar and Generic to express parametric types:
from typing import TypeVar, Generic
T = TypeVar("T")
class Result(Generic[T]):
def __init__(self, value: T, ok: bool) -> None:
self.value = value
self.ok = ok
@classmethod
def success(cls, value: T) -> Result[T]:
return cls(value, ok=True)
@classmethod
def failure(cls, value: T) -> Result[T]:
return cls(value, ok=False)
Use TypeAlias to give complex types a readable name:
from typing import TypeAlias
UserId: TypeAlias = int
Headers: TypeAlias = dict[str, str]
Use @dataclass instead of plain dicts or ad-hoc objects for structured data. Use frozen=True for immutable value objects.
from dataclasses import dataclass, field
from datetime import datetime
@dataclass(frozen=True)
class Money:
amount: int # stored in cents
currency: str
def __post_init__(self) -> None:
if self.amount < 0:
raise ValueError(f"amount must be non-negative, got {self.amount}")
if len(self.currency) != 3:
raise ValueError(f"currency must be a 3-letter ISO code, got {self.currency!r}")
def add(self, other: Money) -> Money:
if self.currency != other.currency:
raise ValueError(f"currency mismatch: {self.currency} vs {other.currency}")
return Money(self.amount + other.amount, self.currency)
@dataclass
class Order:
order_id: str
lines: list[OrderLine] = field(default_factory=list)
created_at: datetime = field(default_factory=datetime.utcnow)
def total(self) -> Money:
if not self.lines:
return Money(0, "USD")
totals = [line.subtotal() for line in self.lines]
return sum(totals[1:], start=totals[0])
field(default_factory=list) is required for mutable defaults — never use lines: list = [] as a default value (it shares the same list across all instances).
__post_init__ runs after __init__ and is the right place for validation.
Use NamedTuple when you need tuple unpacking or a lighter-weight immutable container:
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
origin = Point(0.0, 0.0)
x, y = origin # tuple unpacking works
Use Enum for closed sets of values. Backed enums (str, int) serialize and deserialize cleanly.
from enum import Enum
class Status(str, Enum):
DRAFT = "draft"
ACTIVE = "active"
ARCHIVED = "archived"
def is_published(self) -> bool:
return self is Status.ACTIVE
class Priority(int, Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
Parse from external data with Status(value) — raises ValueError on unknown values, which is usually what you want at a boundary:
raw = request.json["status"]
status = Status(raw) # ValueError if raw is not a valid Status value
Use enums in match/case for exhaustive handling (see next section). Never model a closed set with bare string constants.
Replace long if/elif chains and isinstance checks with match/case. Python's match is structural — it can destructure objects, sequences, and mappings in one expression.
Matching on enum values:
def describe_status(status: Status) -> str:
match status:
case Status.DRAFT:
return "Not yet published"
case Status.ACTIVE:
return "Live and visible"
case Status.ARCHIVED:
return "Hidden from public"
case _:
raise ValueError(f"Unhandled status: {status!r}")
Matching on dataclass structure:
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
Shape = Circle | Rectangle
def area(shape: Shape) -> float:
match shape:
case Circle(radius=r):
return 3.14159 * r * r
case Rectangle(width=w, height=h):
return w * h
case _:
raise TypeError(f"Unknown shape: {shape!r}")
Matching sequences:
def parse_command(tokens: list[str]) -> str:
match tokens:
case ["quit"]:
return "exit"
case ["go", direction]:
return f"moving {direction}"
case ["pick", "up", item]:
return f"picking up {item}"
case [verb, *rest]:
return f"unknown command: {verb} {rest}"
case _:
return "empty input"
Guard clauses with if:
match point:
case Point(x=x, y=y) if x == y:
return "on diagonal"
case Point(x=x, y=y):
return f"at ({x}, {y})"
Replace every os.path.* call with pathlib.Path. Never concatenate path strings with + or / (the string operator).
from pathlib import Path
def read_config(config_dir: Path) -> dict[str, str]:
config_file = config_dir / "config.toml" # / operator for joining
if not config_file.exists():
raise FileNotFoundError(f"Config not found: {config_file}")
return parse_toml(config_file.read_text(encoding="utf-8"))
def ensure_output_dir(base: Path, name: str) -> Path:
out = base / "output" / name
out.mkdir(parents=True, exist_ok=True)
return out
def find_python_files(src: Path) -> list[Path]:
return sorted(src.glob("**/*.py"))
def write_report(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
Always pass encoding="utf-8" to read_text/write_text — do not rely on the platform default. Use Path objects throughout; convert to str only when an external API requires it (str(path)).
Use with for every resource that needs cleanup: files, locks, network connections, database sessions, temporary directories.
from pathlib import Path
def process_file(path: Path) -> list[str]:
with path.open(encoding="utf-8") as fh:
return [line.rstrip() for line in fh]
Write custom context managers with contextlib.contextmanager instead of implementing __enter__/__exit__ on a class, unless the class already exists for other reasons:
from contextlib import contextmanager
from collections.abc import Generator
import time
@contextmanager
def timer(label: str) -> Generator[None, None, None]:
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}s")
with timer("data load"):
records = load_large_dataset()
Use contextlib.suppress to swallow expected exceptions without a bare try/except/pass:
from contextlib import suppress
from pathlib import Path
def remove_if_exists(path: Path) -> None:
with suppress(FileNotFoundError):
path.unlink()
Never use bare except: or except Exception: without re-raising. Always catch the most specific exception type available.
# Prefer
try:
value = int(raw_input)
except ValueError as err:
raise InvalidInputError(f"Expected integer, got {raw_input!r}") from err
# Avoid
try:
value = int(raw_input)
except: # catches SystemExit, KeyboardInterrupt, everything
pass
Define domain exceptions that inherit from Exception (not BaseException):
class AppError(Exception):
"""Base class for all application errors."""
class NotFoundError(AppError):
def __init__(self, resource: str, id: int) -> None:
super().__init__(f"{resource} with id={id} not found")
self.resource = resource
self.id = id
class ValidationError(AppError):
def __init__(self, field: str, message: str) -> None:
super().__init__(f"Validation failed for '{field}': {message}")
self.field = field
Use exception chaining (raise ... from err) to preserve the original cause:
def load_user(user_id: int) -> User:
try:
row = db.fetch_one("SELECT * FROM users WHERE id = %s", (user_id,))
except DatabaseError as err:
raise NotFoundError("User", user_id) from err
if row is None:
raise NotFoundError("User", user_id)
return User.from_row(row)
The from err clause attaches the original exception as __cause__, giving full traceback context without losing information.
| Element | Convention | Example |
|---|---|---|
| Functions and methods | snake_case | calculate_total() |
| Variables and parameters | snake_case | order_id, is_active |
| Classes | PascalCase | OrderService, UserRepository |
| Module-level constants | UPPER_SNAKE_CASE | MAX_RETRIES = 3 |
| Private attributes / methods | _single_underscore prefix | _cache, _validate() |
| Name-mangled attributes | __double_underscore prefix | __secret (use rarely) |
| Type aliases | PascalCase | UserId, Headers |
Module structure — keep this order:
"""Module docstring."""
from __future__ import annotations
# Standard library imports
import os
from pathlib import Path
# Third-party imports
import httpx
# Local imports
from myapp.domain import User
from myapp.exceptions import NotFoundError
# Module-level constants
DEFAULT_TIMEOUT: int = 30
MAX_RETRIES: int = 3
# Public API
__all__ = ["UserClient"]
class UserClient:
...
Prefer composition over inheritance. Keep __init__ (or @dataclass) typed. Use @property for computed attributes that derive from instance state without side effects. Use @classmethod for alternate constructors.
from dataclasses import dataclass
from datetime import date
@dataclass
class DateRange:
start: date
end: date
def __post_init__(self) -> None:
if self.end < self.start:
raise ValueError(f"end ({self.end}) must be >= start ({self.start})")
@classmethod
def for_month(cls, year: int, month: int) -> DateRange:
import calendar
last_day = calendar.monthrange(year, month)[1]
return cls(
start=date(year, month, 1),
end=date(year, month, last_day),
)
@property
def duration_days(self) -> int:
return (self.end - self.start).days + 1
def contains(self, d: date) -> bool:
return self.start <= d <= self.end
@dataclass auto-generates __repr__ and __eq__. When not using @dataclass, always define __repr__ to aid debugging — it should return a string that ideally could be used to recreate the object:
class Money:
def __repr__(self) -> str:
return f"Money(amount={self.amount!r}, currency={self.currency!r})"
Avoid deep inheritance hierarchies. If a class does too many things, extract collaborators and inject them via the constructor (composition). Depend on abstractions (abstract base classes or Protocol) rather than concrete types:
from typing import Protocol
class Notifier(Protocol):
def send(self, recipient: str, message: str) -> None: ...
class OrderService:
def __init__(self, notifier: Notifier) -> None:
self._notifier = notifier
def confirm(self, order: Order) -> None:
# ... business logic ...
self._notifier.send(order.customer_email, f"Order {order.id} confirmed")
Protocol is structural typing — any class with a matching send method satisfies Notifier without explicit inheritance. Prefer it over ABCs for dependency injection boundaries.
npx claudepluginhub aratkruglik/claude-sdlc --plugin python-foundationProvides idiomatic Python style, patterns, and conventions for writing clean, readable code. Covers formatting, naming, and core philosophy.
Python language conventions, modern idioms, and toolchain. Invoke whenever task involves any interaction with Python code — writing, reviewing, refactoring, debugging, or understanding Python projects.
Teaches Pythonic idioms, PEP 8 style, type hints, and best practices for writing readable, maintainable Python code. Useful when writing or reviewing Python code and designing packages.