From casomoltd
Python code generation style rules — Google style with Casomo preferences (types over dicts, EAFP, uv, pyright)
How this skill is triggered — by the user, by Claude, or both
Slash command
/casomoltd:python-style**/*.pyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The rules below are self-contained — apply them directly. They take the
The rules below are self-contained — apply them directly. They take the Google Python Style Guide as the baseline (assume its conventions wherever this file is silent), but what's written here is what's enforced; don't rely on fetching the guide at runtime.
Default output tends to be naive and procedural. Reach for these first — they are the corrections most often needed. Apply them before writing, not after review.
When planning, present the class hierarchy. A design plan should name
the types and how they relate (what subclasses what, what implements
which Protocol, which collections wrap which elements) — not just a
list of functions. Weight that hierarchy toward the collections.abc
ABCs: if a domain concept is a group of things, say which ABC it is
(Mapping/Set/Sequence/Collection) and why, in the plan, before
any code is written.
Separation of concerns. Keep IO, domain logic, and presentation in separate units. A function that fetches and parses and formats is three responsibilities — split them.
Depend on abstractions, not implementations (ports & adapters).
Define the interface the logic needs (a Protocol / type alias) in its
own module; keep the concrete adapter (HTTP client, DB driver) in
another. High-level code imports the interface and receives the adapter
by injection; it never imports the concrete module just to name a type.
Don't mix the abstraction and its implementation in one module. A port
with a single operation is a Callable type alias, not a one-method
Protocol; reserve Protocol for ports with several methods or state.
# ports.py — a single-operation port is just a Callable alias
type Fetch = Callable[[str], str]
# a multi-method port earns a Protocol
class Store(Protocol):
def get(self, key: str) -> bytes: ...
def put(self, key: str, value: bytes) -> None: ...
# client.py — the adapter (imports ports, not vice versa)
def http_get(url: str) -> str: ... # satisfies Fetch structurally
Polymorphism over type codes. An object carrying a kind/type
field that callers if/match on is a missing subtype. Give each
variant a class behind a shared Protocol; let one small factory pick
the type, so call sites never ask "which kind are you?".
# No — callers switch on a type code
if shape.kind == "circle":
area = pi * shape.r**2
elif shape.kind == "square":
area = shape.side**2
# Yes — each shape owns its behaviour
class Shape(Protocol):
def area(self) -> float: ...
Tell, don't ask (delegation). An object should act on its own data
— render, serialize, validate itself — rather than callers reaching in
for its fields. A collection renderer delegates:
"\n".join(str(item) for item in items), not one big function that
picks each item apart. Use __str__ for an object's natural
human/text form (so it formats via f"{obj}" and logs cleanly); use
named methods for alternate formats (e.g. as_html()). Keep the
dataclass-generated __repr__ for debugging — don't conflate the two.
Put behaviour where its data lives. Before adding an operation, ask
which type owns the data it needs, and put it there (order.total(),
report.render(), record.key). Two failure modes to catch: a
category error — hanging an op on a type that doesn't hold its data
(a formatting method on a bare id, a pricing method on a timestamp); and
an aggregate over many items, which belongs to the collection or a
free function, not to a single element (merge(parts), not
part.merge()).
Factory classmethods. Alternate constructors belong on the class as
@classmethod (Order.from_row(...), Report.load(path)), not free
build_order() functions. Bind load/save and class constants
(ClassVar) to the class that owns the data, not scattered module
helpers.
Service factories are make_x() functions. Building a collaborator
— a client that needs a credential, a service that loads config, a cache
that resolves a directory — is a free make_x() factory that
encapsulates the env/config/credential wiring and returns the ready
object to be injected. This is distinct from an alternate constructor
of a value (X.from_row), which stays a classmethod. Don't name it
configure_x — that implies hidden, stateful side effects.
Value objects, not primitives. Don't pass bare str/dict between
modules for a domain concept. Wrap it (NewType, a frozen dataclass, a
model) so the type carries meaning and can grow behaviour.
Identity vs. data. A value object owns its identity and behaviour,
not the heavy artifact it identifies — that lives in a store keyed by the
identity (content-addressing: the key is the content, so a stale entry
can't exist). When identity must persist across runs, derive it from a
stable content hash (sha256(payload).hexdigest()[:16]), never Python
__hash__: hash() of a str/bytes is randomised per process
(PYTHONHASHSEED), so it can't be persisted or compared across runs.
Model collections on collections.abc. When a domain concept is a
group of things, make it a class that subclasses the right ABC rather
than passing a bare list/dict around. Pick the ABC by the access
pattern you actually use, not the storage you happen to keep:
lookup-by-key → Mapping; dedup + membership → Set/Collection;
ordered + indexed → Sequence. The ABC gives you the full interface
(keys/values/get/__contains__, set algebra, …) from a few
methods, and lets the type own domain operations (e.g. unseen,
since). Don't reach for Mapping just because the elements have a
key — only if something genuinely looks them up by it; if you only
iterate and dedupe, that's a Collection/Set.
# ids ARE looked up (set-difference then recover the object) → Mapping
class Records(Mapping[RecordId, Record]):
def __init__(self, items=()): self._by_id = {r.id: r for r in items}
def __getitem__(self, k): return self._by_id[k]
def __iter__(self): return iter(self._by_id)
def __len__(self): return len(self._by_id)
def unseen(self, seen) -> list[Record]:
return [self[k] for k in self if k not in seen]
EAFP — raise at the operation, handle at the boundary. An operation that can't do its job raises; a single resilience boundary catches and decides policy (skip, retry, abort). Never return a sentinel that collides with a valid result.
# No — sentinel hides the failure (an empty list also means "no items")
def parse(text: str) -> list[Item]:
try:
return real_parse(text)
except ParseError:
return []
# Yes — raise; the caller's boundary decides what to do
def parse(text: str) -> list[Item]:
return real_parse(text) # propagates ParseError
Dependency injection over patching. Pass collaborators (a fetcher, a
clock, a path, a client) as parameters with sensible defaults; tests
inject fakes. Needing monkeypatch.setattr(module, ...) means the seam
is missing — add the parameter instead.
Framework-first. Before hand-rolling serialization, validation,
parsing, or config handling, use the library that does it. A
hand-written to_dict/from_dict pair is usually a model class you
haven't reached for yet.
Simplest representation; don't over-build (YAGNI). Model data with the
plainest type that serves the access you actually have, and add structure
only when a concrete need forces it — not speculatively. Two tells: a
bespoke layer that re-implements what a standard library or built-in
already does (that's Framework-first — don't re-solve a solved problem),
and an abstraction wrapping data a list/dict/dataclass already models.
Prefer net deletion; don't add a type whose only job is to host one
method.
Keep a policy in one place. The two halves of one policy — encode and decode, serialise and parse, open and close, a format and its parser — are a single decision and belong together (one class, one module, one construction site). Split them and they drift onto different assumptions: one half quietly relies on something the other no longer guarantees. Define the policy once and route both directions through it.
No hardcoded config. Domain values — URLs, hosts, addresses,
limits, paths — live once in a config module and are imported, never
re-typed as literals scattered across modules. If a value appears in
two places, derive one from the other (USER_AGENT = f"…{BASE_URL}"),
don't repeat it.
Prefer NamedTuple or @dataclass over plain dicts for structured data.
Use dicts only for genuinely dynamic key-value mappings (e.g. JSON payloads
you're passing through without inspecting).
# Yes
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
# Yes
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
# No
point = {"x": 1, "y": 2}
Use NamedTuple only for a plain immutable, read-only record with no methods.
Once the type grows methods or validation, prefer @dataclass(frozen=True): it
stays immutable while giving behaviour a clear home. Use a mutable @dataclass
only when you genuinely need to reassign fields.
lower_with_underCapWordslower_with_under()CAPS_WITH_UNDER___ for name manglingid, url, db)import x for packages/modules; from x import y for specific symbolsfrom . import x,
from .models import Y, from ..config import Z); reserve absolute
imports for stdlib and third-party. External consumers (tests, scripts,
other packages) import the package absolutely. Rationale: the package is
relocatable, and internal vs external dependencies read at a glance.
Neither ruff nor pyright can require relative imports (ruff's TID251
resolves relative imports to their absolute path and bans both), so
enforce it with a small AST guard test that flags absolute self-imports
inside the package.if __name__ == "__main__": blocks, and a
heavy or optional third-party dependency imported lazily inside the
factory/method that uses it, so commands that never touch it don't pay its
import cost at startup (e.g. a CLI whose --help or validation path
shouldn't import a heavy cloud or ML SDK it never calls). Enforce the rule
globally with ruff PLC0415, and
grant a commented per-file-ignore to the one module that does the lazy
import — keep that ignore list short and justified so the deviation stays
visible.__all__ — the names it exports. Anything not in
__all__ is internal to the module.__all__ and the _ prefix agree: public names have no leading
underscore and appear in __all__; internal helpers, classes, and
constants start with _ and are omitted. (logger is the conventional
exception — module-level, never exported.)__init__.py re-exports the package's public API and
declares its own __all__, so callers import from the package, not its
submodules.__all__ sorted (ruff RUF022 enforces this).X | None not Optional[X]list[str], dict[str, int], tuple[int, ...]collections.abc for abstract types: Sequence, Mapping, Iterableself, cls, or __init__ returnarg: str = "default"Google-style docstrings with """triple double quotes""".
def load_records(path: Path) -> list[Record]:
"""Load records from a CSV file.
Args:
path: Path to the CSV file.
Returns:
List of Record objects, one per row.
Raises:
FileNotFoundError: If the file does not exist.
"""
Args:, Returns:, Raises: sections with hanging indent.None then initialise inside.operator module over trivial lambdas._-prefixed)
ones. Lead with the public API (constants, classes, functions);
private helpers and private constants go below. Forward references
resolve at call time, so a public function may call a private helper
defined further down — that's fine and expected._) methods.main() entry point and the if __name__ == "__main__": guard
stay at the bottom.ValueError, TypeError, FileNotFoundError, etc.__post_init__ for dataclasses) and raise, so
an object cannot exist in an invalid state — don't return sentinel values.raise ... from None to suppress an irrelevant chained exception.except:. Never catch generic Exception unless re-raising.try blocks.with statements for files, sockets, and any resource that needs cleanup.# Yes (EAFP)
try:
value = mapping[key]
except KeyError:
value = default
# No (LBYL)
if key in mapping:
value = mapping[key]
else:
value = default
"double quotes").%-style placeholders, not f-strings.print is for program output — the data the user asked for. Status,
progress, and diagnostics go through a module logging logger, never
print.88 characters max. Exceptions: long imports, URLs.
for clauses — use a regular loop instead.Every script must have:
if __name__ == "__main__":
main()
No top-level execution besides imports and definitions.
test_<subject>_should_<expected> — e.g.
test_press_self_destruct_should_go_bang. The list of test names
should read like a specification of the behaviour.def test_press_self_destruct_should_go_bang():
"""Pressing self-destruct detonates the reactor.
Arm the reactor, press self-destruct, and assert it detonates.
Guards the safety contract that an armed reactor must respond to the
self-destruct command (regression: PR #42 swallowed the signal).
"""
reactor = Reactor(armed=True)
reactor.press_self_destruct()
assert reactor.detonated
uv sync, uv run … (the venv lives at
.venv/). Don't call pip directly or edit requirements*.txt.ruff (configured in pyproject.toml); all code must pass
ruff check with zero errors before committing.pyright with
zero errors before committing.Offers UI/UX design guidance for web and mobile with 50+ styles, 161 color palettes, 57 font pairings, and 99 UX guidelines across 10 stacks. Use for designing pages, components, color systems, or reviewing UI code.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub casomoltd/tooling --plugin casomoltd