From predicate
Provides idiomatic Python style, patterns, and conventions for writing clean, readable code. Covers formatting, naming, and core philosophy.
How this skill is triggered — by the user, by Claude, or both
Slash command
/predicate:pythonThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
stdlib before adding dependenciesLet the formatter handle it. Pick one formatter project-wide and enforce it in CI.
ruff check . # Lint
ruff format . # Format
black . # Alternative formatter
| 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. |
| 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. |
Functions are actions → verbs. Variables are data → nouns. If a function name is a noun, it's probably a property.
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.
| ✅ 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 |
| 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 |
| 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
# ✅ 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:
...
* to force keyword-only arguments when order could be confused/ (3.8+) for positional-only when the parameter name is an implementation detailNone explicitly when a function can return None — don't rely on implicit fallthrough# ❌ 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
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
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
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
itertoolsReach 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 |
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())
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/awaitPrefer 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()))
| 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 |
asyncio.run() with a running loop — it raises RuntimeErrorto_thread — it blocks the entire loopasync for for async iteration and async with for async context managersasyncio.CancelledError explicitly when cleanup is neededmatch/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}")
| 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.
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.
int | str not Union[int, str], str | None not Optional[str] (3.10+)| 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 objectAny disables type checking. object is type-safe — it requires narrowing before use. Prefer object when you mean "anything" but still want the checker engaged.
TYPE_CHECKING PatternAvoid 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:
...
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
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}")
str.format() — use only when format spec is dynamic (known at runtime, not write-time)% formatting — legacy only. Do not use in new code.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
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
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-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 |
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
snake_case — functions, variables, modulesPascalCase — classes onlyif x is None — not == Noneif not seq — not len(seq) == 0@dataclass — not manual __init__with — for any resource needing cleanupint | str — not Union[int, str] (3.10+)* separator — force keyword-only args when order is ambiguousid, type, list, dict, etc.except: pass — catch specific, handle or propagateThese idioms refine but are subordinate to the Code-Edit Constraints.
npx claudepluginhub nrdxp/predicate --plugin predicateTeaches 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.
Python language conventions, modern idioms, and toolchain. Invoke whenever task involves any interaction with Python code — writing, reviewing, refactoring, debugging, or understanding Python projects.
Guides Python development with idiomatic patterns, PEP 8 standards, type hints, and best practices for readable, maintainable code.