python

Idiomatic programming style, patterns, and conventions for Python development. Trigger when: - Writing, refactoring, reviewing, or debugging Python code. - Files matching the pattern **/*.py (includin

SKILL.md

GitHub

Python Language Idioms

Work with the language. Lean into duck typing, generators, context managers, and the standard library. Write code that reads like pseudocode — if it needs a comment to explain what it does, rewrite it.


Core Philosophy

  • Readability is a feature — code is read far more than it is written; optimize for the reader
  • Explicit over implicit — no magic, no hidden state, no clever tricks
  • Flat over nested — avoid deep indentation; early returns, guard clauses, comprehensions
  • Composition over inheritance — functions, protocols, and dataclasses beat class hierarchies
  • Standard library first — reach for builtins and stdlib before adding dependencies

Formatting

Let the formatter handle it. Pick one formatter project-wide and enforce it in CI.

ruff check .          # Lint
ruff format .         # Format
black .               # Alternative formatter

Non-Negotiables

Rule Standard
Indentation 4 spaces. Tabs are prohibited.
Operator spacing x = y * 12 + 13, not x=y*12+13
Quote style Pick single or double project-wide; enforce via formatter. PEP 8 is deliberately silent.
Line length Configure in formatter (88 for black/ruff, 79 for strict PEP 8). Don't manually wrap.
Imports isort-ordered: stdlib → third-party → local. One import per line for from imports.

Naming

Conventions

Scope Style Example
Modules, packages snake_case crypto_key.py, utils/
Functions, methods snake_case (verbs) get_url(), calculate_checksum()
Variables snake_case (nouns) raw_payload, user_registry
Classes PascalCase HttpClient, TokenParser
Constants SCREAMING_SNAKE MAX_RETRIES, DEFAULT_TIMEOUT
Private _leading_underscore _internal_cache, _validate()
Name-mangled __double_leading __secret (rarely needed)
Dunder __name__ Reserved for the language. Never invent new ones.

The Verb-Noun Rule

Functions are actions → verbs. Variables are data → nouns. If a function name is a noun, it's probably a property.

Shadowing Built-ins (Prohibited)

Redefining these names breaks built-in functionality within scope and causes non-deterministic bugs:

id, type, len, range, list, dict, str, int, float, min, max, abs, set, map, filter, input, open, hash, format, next, iter, sum, any, all, dir, vars, help

Use descriptive alternatives: user_id, item_type, name_list, count.

Natural Phrasing

✅ Idiomatic ❌ Avoid
if x not in y if not x in y
if x != y if not x == y
if x is not None if not x is None

Boolean and Comparison Idioms

Scenario ❌ Wrong ✅ Idiomatic Why
Truthiness if x == True: if x: if evaluates truthiness directly
Falsiness if x == False: if not x: not is the logical inverter
None check if x == None: if x is None: is checks identity; == can be overridden by __eq__
Empty check if len(seq) == 0: if not seq: Empty collections are falsy
Type check type(x) == int isinstance(x, int) Respects inheritance

Data Modeling

When to Use What

Type Use When Key Trait
@dataclass Mutable data with behavior Auto-generates __init__, __repr__, __eq__
@dataclass(frozen=True) Immutable value objects Hashable, safe as dict keys
NamedTuple Lightweight immutable records Tuple-compatible, unpacking works
TypedDict Typed dict shapes (JSON, APIs) Runtime is still a plain dict
Plain dict Dynamic/unknown keys No structure guarantees
from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)
class Token:
    kind: str
    value: str
    line: int = 0

__slots__

Use slots=True on dataclasses (3.10+) or define __slots__ manually. It prevents dynamic attribute creation, reduces memory, and speeds up attribute access:

# ✅ With slots — fixed attributes, lower memory
@dataclass(slots=True)
class Point:
    x: float
    y: float

# ❌ Without slots — __dict__ per instance, allows typos like p.z = 3

Functions

Arguments and Returns

# ✅ Type-annotated at boundaries
def parse_config(raw: str, *, strict: bool = False) -> Config:
    ...

# ✅ Keyword-only after * — prevents positional misuse
def connect(host: str, port: int, *, timeout: float = 30.0) -> Connection:
    ...
  • Use * to force keyword-only arguments when order could be confused
  • Use / (3.8+) for positional-only when the parameter name is an implementation detail
  • Return None explicitly when a function can return None — don't rely on implicit fallthrough

Default Mutable Arguments (Critical)

# ❌ DANGEROUS — shared across all calls
def append_to(item, target=[]):
    target.append(item)
    return target

# ✅ Sentinel pattern
def append_to(item, target: list | None = None):
    if target is None:
        target = []
    target.append(item)
    return target

Properties

Use @property for computed attributes. Use @cached_property (3.8+) when the result is expensive and stable:

class Circle:
    def __init__(self, radius: float):
        self.radius = radius

    @property
    def area(self) -> float:
        return math.pi * self.radius ** 2

Iterators and Comprehensions

Comprehensions

Prefer comprehensions for simple transforms. Use explicit loops when the logic needs if/else branching or side effects:

# ✅ Clear — single transform + filter
names = [u.name for u in users if u.is_active]

# ✅ Dict comprehension
lookup = {u.id: u for u in users}

# ✅ Set comprehension
unique_tags = {tag for post in posts for tag in post.tags}

# ❌ Nested comprehension that requires mental parsing
result = [f(x) for x in [g(y) for y in items if h(y)] if p(x)]
# → Use intermediate variables or a generator pipeline instead

Generators

Use generators for lazy evaluation over large or infinite sequences. They consume O(1) memory:

# ✅ Generator expression — lazy, memory-efficient
total = sum(order.amount for order in orders)

# ✅ Generator function — for complex logic
def read_chunks(path: str, size: int = 8192):
    with open(path, 'rb') as f:
        while chunk := f.read(size):
            yield chunk

itertools

Reach for itertools before rolling your own iteration logic:

Function Purpose
chain Concatenate iterables
islice Slice without materializing
groupby Group sorted items by key
product Cartesian product
starmap map() with argument unpacking
batched (3.12+) Fixed-size chunks

Context Managers

Use with for any resource that needs cleanup — files, locks, connections, transactions:

# ✅ Automatic cleanup
with open('data.json') as f:
    data = json.load(f)

# ✅ Multiple resources
with open('in.txt') as src, open('out.txt', 'w') as dst:
    dst.write(src.read())

Writing Context Managers

For simple cases, use contextlib.contextmanager:

from contextlib import contextmanager

@contextmanager
def temporary_directory():
    path = tempfile.mkdtemp()
    try:
        yield path
    finally:
        shutil.rmtree(path)

For classes, implement __enter__ and __exit__. For async resources, use async with and __aenter__ / __aexit__.


Async Patterns

async/await

Prefer async/await for I/O-bound concurrency:

import asyncio

async def fetch_user(session: aiohttp.ClientSession, uid: str) -> User:
    async with session.get(f'/api/users/{uid}') as resp:
        resp.raise_for_status()
        return User(**(await resp.json()))

Concurrent Operations

Pattern Behavior Use When
asyncio.gather(*coros) Runs concurrently, fails fast All must succeed
asyncio.TaskGroup (3.11+) Structured concurrency, auto-cancel Prefer over gather
asyncio.to_thread(fn) Offloads blocking I/O to thread pool Wrapping sync libraries

Rules

  • Never mix asyncio.run() with a running loop — it raises RuntimeError
  • Never call blocking I/O in an async function without to_thread — it blocks the entire loop
  • Use async for for async iteration and async with for async context managers
  • Cancellation: Handle asyncio.CancelledError explicitly when cleanup is needed

Structural Pattern Matching (3.10+)

match/case is a structural destructuring tool, not a switch statement. It combines type checking, attribute extraction, and branching in one expression:

match command:
    case {"action": "move", "direction": str(d)}:
        move(d)
    case {"action": "quit"}:
        sys.exit(0)
    case Point(x=0, y=y):
        print(f"On y-axis at {y}")
    case [first, *rest] if len(rest) > 2:
        process_batch(first, rest)
    case _:
        raise ValueError(f"Unknown command: {command}")

Key Facts

Fact Detail
Single subject match targets exactly one variable — no scattered conditions
Destructuring Sequences, mappings, and class attributes are unpacked declaratively
Guards case X if condition: for fine-grained filtering
Wildcard _ Can appear multiple times in one pattern (unlike assignment)
Class matching Uses __match_args__ or @dataclass for positional patterns

[!WARNING] Strings are NOT sequences in match/case. Unlike standard unpacking, case [x, y] will never match a two-character string. This is intentional — it prevents a class of bugs where strings are accidentally iterated as character arrays.


Type System

Python's type system is gradual — annotations are optional but invaluable for static analysis. The runtime remains dynamic; types don't enforce anything at execution time.

Annotation Rules

  • Annotate function signatures — parameters and return types
  • Let inference handle locals — don't annotate obvious assignments
  • Use modern syntax — int | str not Union[int, str], str | None not Optional[str] (3.10+)

Key Constructs

Construct Use
type Vector = list[float] (3.12+) Type alias — equivalent name for a type
NewType('UserId', int) Distinct subtype — a raw int won't satisfy UserId
Protocol Structural subtyping ("static duck typing")
@runtime_checkable Enables isinstance checks against a Protocol
TypeIs (3.13+) Preferred over TypeGuard — supports intersection narrowing and negative narrowing
TypeGuard Older narrowing — only narrows in the positive branch

Any vs object

Any disables type checking. object is type-safe — it requires narrowing before use. Prefer object when you mean "anything" but still want the checker engaged.

The TYPE_CHECKING Pattern

Avoid import cycles by guarding type-only imports:

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from expensive_module import HeavyType

def process(item: HeavyType) -> None:
    ...

String Handling

f-strings (Default)

f-strings are the standard for string interpolation. They are fast, readable, and support format specs:

name = "world"
print(f"Hello, {name!r}")       # repr
print(f"Pi is {math.pi:.4f}")   # format spec
print(f"{value:>10,}")          # right-aligned with comma separator

Template Strings (3.14+)

T-strings (t"...") produce a Template object instead of a str, enabling handler-based interpolation. Use them when interpolating untrusted input — f-strings evaluate eagerly and cannot be intercepted:

from string.templatelib import Template, Interpolation

def sanitized_sql(template: Template) -> tuple[str, tuple]:
    parts, params = [], []
    for item in template:
        if isinstance(item, str):
            parts.append(item)
        elif isinstance(item, Interpolation):
            parts.append("?")
            params.append(item.value)
    return "".join(parts), tuple(params)

query, params = sanitized_sql(t"SELECT * FROM users WHERE id = {user_input}")

Legacy

  • str.format() — use only when format spec is dynamic (known at runtime, not write-time)
  • % formatting — legacy only. Do not use in new code.

Error Handling

Specific Exceptions

Catch specific exceptions. Never use bare except: or except Exception: pass:

# ✅ Specific, with context
try:
    config = load_config(path)
except FileNotFoundError:
    raise ConfigError(f"Config not found: {path}") from None
except json.JSONDecodeError as e:
    raise ConfigError(f"Malformed config at {path}: {e}") from e

# ❌ Silent swallow — hides every possible failure
try:
    config = load_config(path)
except:
    pass

Exception Chaining

Use from to preserve the causal chain. Use from None to intentionally suppress it:

# Chain — preserves original traceback
raise AppError("operation failed") from original_error

# Suppress — when the original is noise for the caller
raise AppError("not found") from None

Custom Exceptions

Define domain-specific exceptions. Keep hierarchies shallow:

class AppError(Exception):
    """Base for application errors."""

class ValidationError(AppError):
    def __init__(self, field: str, message: str):
        self.field = field
        super().__init__(f"{field}: {message}")

Anti-Patterns

Anti-Pattern Description Remedy
Mutable default args def f(x=[]): — shared across calls Use None sentinel
Bare except except: catches SystemExit, KeyboardInterrupt Catch specific types
Shadowing builtins list = [1, 2, 3] Use descriptive names
God class Monolithic class with 20+ methods Decompose into functions and smaller classes
Stringly typed Using strings where enums or types belong enum.Enum, NewType, Literal
Deep nesting 4+ levels of indentation Early returns, guard clauses, helper functions
import * Pollutes namespace, breaks tooling Explicit named imports
Type: ignore spam Silencing every type error Fix the types or narrow properly
Overusing classes Classes with no state (just methods) Use plain functions
isinstance chains Long if isinstance(x, A) ... elif isinstance(x, B) match/case or dispatch

Tooling

ruff check . --fix    # Lint + autofix
ruff format .         # Format
mypy .                # Type check (strict)
pyright .             # Alternative type checker
pytest                # Run tests
pytest --cov          # Coverage
uv run ...            # Fast dependency management and execution

Quick Reference

  • 4-space indent — never tabs, never 2-space
  • snake_case — functions, variables, modules
  • PascalCase — classes only
  • if x is None — not == None
  • if not seq — not len(seq) == 0
  • @dataclass — not manual __init__
  • with — for any resource needing cleanup
  • Comprehensions — for simple transforms, loops for complex logic
  • Generators — for lazy iteration over large data
  • f-strings — default interpolation; T-strings for untrusted input
  • int | str — not Union[int, str] (3.10+)
  • * separator — force keyword-only args when order is ambiguous
  • Never shadow builtins — id, type, list, dict, etc.
  • Never except: pass — catch specific, handle or propagate

These idioms refine but are subordinate to the Code-Edit Constraints.