Skip to content
cccskills
FreePublished on GitHub

python-conventions

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.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md14.1 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Python Conventions (stack-agnostic, Python 3.10+)

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).

Detection

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.

Type hints — always on

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]

Dataclasses — prefer over plain dicts

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

Enums — replace magic constants

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.

match/case — structural pattern matching

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})"

pathlib — all filesystem paths

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)).

Context managers

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()

Exception discipline

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.

Naming conventions (PEP 8)

ElementConventionExample
Functions and methodssnake_casecalculate_total()
Variables and parameterssnake_caseorder_id, is_active
ClassesPascalCaseOrderService, UserRepository
Module-level constantsUPPER_SNAKE_CASEMAX_RETRIES = 3
Private attributes / methods_single_underscore prefix_cache, _validate()
Name-mangled attributes__double_underscore prefix__secret (use rarely)
Type aliasesPascalCaseUserId, 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:
    ...

Class design

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.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc42Updated Oct 17, 2026

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc42Updated Oct 17, 2026

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc42Updated Oct 17, 2026

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc42Updated Oct 17, 2026

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc42Updated Oct 17, 2026

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc42Updated Oct 17, 2026

See all skills by AratKruglik

Report a problem with this skill