
Write Python
- 1 installs
- 2 repo stars
- Updated July 23, 2026
- crpier/dotfiles
Helps with python tasks.
About
write-python is a Claude Code skill for python. It helps developers move faster with AI-assisted coding.
- write-python
- Python
- AI-coding skill
Write Python by the numbers
- 1 all-time installs (skills.sh)
- Ranked #240 of 290 Python skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/crpier/dotfiles --skill write-pythonAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 2 |
| Last updated | July 23, 2026 |
| Repository | crpier/dotfiles ↗ |
What it does
Helps with python tasks.
Files
Write Python
Use this skill whenever producing or changing Python code. These rules are hard requirements unless they use softer words like should or prefer. Prefer consistency with surrounding code only where it does not conflict with a hard requirement.
Core requirements
- Add type hints to all functions, methods, and class attributes.
- Use Python 3.10+ type syntax:
str | int,list[str],dict[str, int]. - Keep lines to 88 chars, except long log/error messages where
E501is
allowed.
- Start every non-empty module with a module docstring.
- Avoid import-time work. Never perform IO at import time.
- Use async APIs for all IO. If no async API exists, run blocking work in an
executor.
- Use timezone-aware datetimes:
datetime.now(UTC), never bare
datetime.now().
Naming, design, and errors
- Prefer explicit, domain-rich names:
attribute_index,database_session,
cluster_status; avoid abbreviations and one-letter names.
- A name should add information. If the receiving keyword already gives a value
its meaning, do not create a temporary name just to repeat it.
- Do not introduce single-use local variables for simple literals or simple
constructors when the value is immediately passed to a named argument. Inline the value instead. A temporary variable is appropriate only when it is reused, the expression is complex enough to obscure the call site, or the name adds domain meaning that is not already present in the destination parameter.
- Avoid generic single-use temporaries like
timestamp,data,result,
value, or payload when they only restate the type or shape of the value. Either inline the expression or use a domain name such as event_happened_at, published_at, or retry_deadline.
- Use
get_only for pure non-IO functions. Usefetch_for IO that returns a
value.
- Prefer custom domain types over primitives for validated domain concepts.
- Prefer locality of behavior over aggressive DRY.
- Do not introduce an abstraction until there are at least two implementations.
- Do not create passthrough, one-line, or tiny single-use functions unless
they clarify genuinely complex logic. Never create a one-line helper solely to satisfy a lint rule.
- Keep helpers local to their scope: one class -> private method/staticmethod;
one method -> nested function or inline.
- Prefer context managers over manual resource management.
- Raise project/domain exceptions, never stdlib exceptions. Use
raise DomainError(...) from e when wrapping another exception.
- For Ruff TRY301 (
raise-within-try), restructure production code to avoid
raising inside try; if truly unavoidable, ignore the raise line with # noqa: TRY301. Do not create a one-line function that only raises.
- Convert domain exceptions to HTTP exceptions at API boundaries.
Single-use temporaries
Prefer:
pending_event = Event(
enabled=True,
happened_at=datetime(2026, 1, 2, 3, 4, 5, 678901, tzinfo=UTC),
payload={"ok": True},
)Avoid:
timestamp = datetime(2026, 1, 2, 3, 4, 5, 678901, tzinfo=UTC)
pending_event = Event(
enabled=True,
happened_at=timestamp,
payload={"ok": True},
)Imports and public APIs
- Use absolute imports only. Never use relative imports.
- Prefer
from package import Namesyntax. - Use bare
import moduleonly for name conflicts or these stdlib modules:
os, sys, time, logging, asyncio, pathlib, subprocess, threading, contextlib.
- Do not alias stdlib imports; alias third-party imports instead when needed.
__init__.pyfiles should either curate__all__or be empty.- Keep
__all__sorted lexically and update it when exports change, including
renames and refactors.
Ordering and classes
- At module scope, order functions as private functions, then public functions;
within those groups, define called helpers before callers.
- In classes, order methods as
__init__/__new__, ABC/protocol methods,
public methods, private methods.
- Only inside classes, define caller methods before called methods.
- In regular classes, declare instance attributes in
__init__with explicit
types: self.name: str = name.
- Do not declare regular instance attributes in the class body unless using a
dataclass, Pydantic model, or similar declarative structure.
- Sort fields alphabetically in dataclasses, Pydantic models, and similar
structures unless semantic grouping is clearer.
Docstrings, comments, and logging
- Most functions and classes should have docstrings. Constants should almost
always have docstrings.
- Public functions and classes, especially classes, should have runnable Python
usage examples in docstrings.
- Private helpers should have docstrings when they encode non-obvious behavior,
domain rules, validation rules, state transitions, SQL/query compilation, concurrency behavior, or error translation.
- Do not require docstrings for tiny local helpers whose name fully explains
their behavior.
- Private-helper docstrings should explain why the helper exists and what
invariant it protects, not restate each line of code.
- Prefer this shape for internal helpers:
def _compile_predicates_sql(...) -> ...:
"""Compile accumulated where() predicates as AND-ed SQL fragments.
Query builders store repeated where() calls as separate predicates so the
explicit-filter intent remains observable until compilation.
"""- Explain the
whatand/orwhy; explain thehowonly if surprising. - Do not include
Args:,Returns:, orRaises:sections. - Use triple double quotes; no blank line after opening quotes or before
closing quotes.
- Avoid comments that restate code. Comments should tell a story.
- TODO forms:
# TODO:may merge;# FIXME:may commit but not merge;
# XXX: must be fixed before committing.
- Use structured logging with keyword context, not
%sinterpolation. - Bind stable logging context once when multiple log lines share attributes.
Testing
- Use
snektestwith@test()and typed test functions. - Tests must verify one behavior at a time. Do not write “full surface”,
“end-to-end everything”, or umbrella tests that combine multiple independent behaviors like insert + select result shapes + update + delete in one test. Split them into focused tests with names that state the single behavior under test.
- Setup may use supporting operations, but assertions should target one
behavior. If a test has unrelated assertion groups, split it.
- Tests must not mix setup, behavior under test, and assertions in the same
logical block. Use fixtures or clearly separated helper setup when setup is non-trivial, especially for database state.
- If a test name contains words like “full”, “surface”, “and”, or lists
multiple verbs, challenge whether it should be multiple tests.
- Prefer separate tests for result-shape behavior, mutation behavior, error
behavior, lifecycle behavior, and backend policy behavior.
- Use test function docstrings to explain the case.
- Load fixtures at the top of the test body, immediately after the docstring.
Mid-test load_fixture(...) is a smell; move it up or split the test.
- Use fixtures to create external resources and seed prerequisite state.
- The test body should make the behavior under test obvious.
- Avoid doing setup inserts, the mutation under test, and result
fetching/assertion all inside one transaction/block unless the transaction boundary itself is the behavior under test.
- Define local classes under test after external fixture acquisition.
- Prefer fakes for external services and test databases for database behavior.
- In tests, ignore Ruff TRY301 violations on the same line with
# noqa: TRY301; do not extract a one-line raising helper.
- Avoid
cast()in tests; it usually indicates poor testability.
References
- Open REQUIREMENTS.md for nuance and edge cases.
- Open EXAMPLES.md when applying an unfamiliar convention or
reviewing ambiguous code.
Python Requirements Examples
Use these examples when a convention is unfamiliar or ambiguous. They are not a complete restatement of the requirements.
Docstrings
Good:
"""Given a task function, dynamically build a
pydantic model that can validate its payload."""Bad:
"""Given a task function, dynamically build a pydantic model that can validate
its payload."""Structured logging
Good:
logger.error("Failed to delete cluster.", cluster_name=cluster_name)Bad:
logger.error("Failed to delete cluster %s", cluster_name)Bind repeated context once.
Good:
logger = logger.bind(cluster_id=cluster_id, user_id=user_id)
logger.info("Starting provisioning")
logger.info("Provisioning finished")Bad:
logger.info("Starting provisioning", cluster_id=cluster_id, user_id=user_id)
logger.info("Provisioning finished", cluster_id=cluster_id, user_id=user_id)Imports
Use absolute imports.
Good:
from package.module import functionBad:
from .module import function
from ..package import functionAlias third-party imports instead of stdlib imports.
Bad:
from dataclasses import dataclass as std_dataclass
from pydantic.dataclasses import dataclassGood:
from dataclasses import dataclass
from pydantic.dataclasses import dataclass as pydantic_dataclassTimezone-aware datetimes
Good:
from datetime import UTC, datetime, timedelta
expires_at = datetime.now(UTC) + timedelta(hours=DEFAULT_TTL_HOURS)Bad:
expires_at = datetime.now() + timedelta(hours=DEFAULT_TTL_HOURS)Domain types
Bad:
def add_domain(domain: str) -> None:
...Good:
from annotations import ASCIIDomain
def add_domain(domain: ASCIIDomain) -> None:
...Regular class attributes
Bad:
class MyClass:
name: str
def __init__(self, name: str) -> None:
self.name = nameGood:
class MyClass:
def __init__(self, name: str) -> None:
self.name: str = nameTests with local classes
Bad:
class ClassUnderTest(Base):
__table_name__: ClassVar[str] = "clusters"
@test()
def ClassUnderTest_is_validated() -> None:
...Good:
@test()
def ClassUnderTest_is_validated() -> None:
class ClassUnderTest(Base):
__table_name__: ClassVar[str] = "clusters"
...Fixture acquisition order
Good:
@test()
def QueryBuilder_compiles_explicit_filters() -> None:
"""Compile explicit filters without merging them with runtime behavior."""
fixture = load_fixture("query_builder_explicit_filters")
class QueryBuilderUnderTest(QueryBuilder):
...
result = QueryBuilderUnderTest(fixture).compile()
assert result == fixture.expected_sqlBad:
@test()
def QueryBuilder_compiles_filters_and_sqlite_runtime() -> None:
"""Compile filters and check SQLite runtime behavior."""
class QueryBuilderUnderTest(QueryBuilder):
...
query_builder = QueryBuilderUnderTest()
filter_fixture = load_fixture("query_builder_explicit_filters")
runtime_fixture = load_fixture("sqlite_runtime")
assert query_builder.compile(filter_fixture) == filter_fixture.expected_sql
assert sqlite_runtime_accepts(runtime_fixture)Arrange/Act/Assert separation
Good:
@test()
async def UserRepository_updates_selected_users() -> None:
"""Update matching users without testing unrelated result-shape behavior."""
database = await load_fixture(database_with_seeded_users())
async with database.transaction() as tx:
await tx.execute(update(User).set(active=False).where(User.age < 18))
async with database.transaction() as tx:
rows = await tx.fetch_all(select(User.id, User.active).all())
assert_eq(rows, [("ada", True), ("grace", False)])Bad:
@test()
async def UserRepository_full_insert_update_and_select_surface() -> None:
"""Insert, update, fetch result shape, and assert lifecycle behavior."""
async with database.transaction() as tx:
await tx.execute(insert(User).values(id="grace", age=17))
await tx.execute(update(User).set(active=False).where(User.age < 18))
rows = await tx.fetch_all(select(User.id, User.active).all())
assert_eq(rows, [("grace", False)])
assert await tx.fetch_one(select(count()).from_(User)) == 1Lint suppressions
Good:
def method(self, unused_argument: str) -> None: # noqa: ARG002 - protocol
...For TRY301 in tests, suppress the line instead of extracting a raise-only helper.
Good:
@test()
def DatabaseUnavailableError_is_caught() -> None:
caught_error: DatabaseUnavailableError | None = None
try:
raise DatabaseUnavailableError("down") # noqa: TRY301
except DatabaseUnavailableError as e:
caught_error = e
assert caught_error is not NoneBad:
def raise_database_unavailable_error() -> None:
raise DatabaseUnavailableError("down")
@test()
def DatabaseUnavailableError_is_caught() -> None:
caught_error: DatabaseUnavailableError | None = None
try:
raise_database_unavailable_error()
except DatabaseUnavailableError as e:
caught_error = e
assert caught_error is not NonePydantic
Good:
from typing import ClassVar
from pydantic import BaseModel
class MyModel(BaseModel):
name: str
model_config = {
"extra": "forbid",
}Opinionated Python Requirements
This file adds nuance that is too detailed for SKILL.md. The rules are hard requirements unless they use softer words like should or prefer.
How to apply these requirements
Local consistency matters, but it never overrides a hard requirement. If a file already violates a hard requirement, do not spread the pattern. Prefer the smallest change that improves the touched code without forcing unrelated churn.
When requirements conflict, prioritize in this order:
1. Correctness and safety. 2. Public API compatibility. 3. Hard requirements in SKILL.md. 4. Local consistency. 5. Soft preferences.
Type hints
Annotate private code too. Missing annotations are not acceptable just because a function is private, nested, or test-only.
Prefer specific types over Any. If Any is necessary at a boundary, keep it localized and convert it to domain types as soon as practical.
Avoid cast(). If it is necessary, add a nearby comment explaining why the cast is safe and why a more precise type is not practical. Prefer cast() over a broad type: ignore, but do not use either casually.
Constants and docstrings
Constants should almost always have docstrings or be self-explanatory in a very small local scope. Use a docstring when the value encodes policy, tuning, protocol behavior, or a domain assumption.
Function and class docstrings should explain the what and/or why. Explain implementation details only when behavior is surprising or easy to misuse. Do not use generated-looking Args:, Returns:, or Raises: sections.
Wrapped docstring lines should be balanced rather than leaving one very long line followed by a tiny fragment.
Naming
Use explicit names over abbreviations. Prefer names that encode domain meaning, such as database_session, request_logger, or cluster_status.
Avoid bare id when a more precise name exists, such as user_id or cluster_id.
Conventional short names are allowed only in very small, conventional scopes, such as async with self.database as tx:.
Use get_ only for pure, non-IO functions. Use fetch_ when returning a value requires IO, even if the IO is hidden behind a client or repository object.
Imports
Absolute imports make dependencies explicit and reduce ambiguity during refactors. Do not use relative imports, including from .module import Name.
Prefer from package import Name because it makes used names explicit. Bare import module is reserved for name conflicts and the approved stdlib modules listed in SKILL.md.
Do not alias standard library imports. If a stdlib name conflicts with a third-party name, alias the third-party import.
Public APIs
Use package __init__.py files to define meaningful public APIs. If a package has no curated public surface, keep __init__.py empty. Do not leave an __init__.py containing only a docstring.
Keep __all__ sorted lexically. Update it during renames, moves, and refactors, not only when adding new exports.
Async and IO
All IO must be async. This includes network calls, subprocesses, filesystem work, database calls, and calls into SDKs that perform IO.
If a dependency has no async API, isolate the blocking call and run it through an executor using asyncio. Do not hide blocking IO inside an async def.
Never perform IO at import time. Importing a module should not open files, connect to services, read environment-dependent remote state, start background tasks, or do expensive computation.
Error handling
Raise project or domain exceptions instead of stdlib exceptions. Wrapping an underlying exception is fine when it preserves useful causality: raise DomainError(...) from e.
Catch exceptions at the level that has enough context to handle, log, retry, or translate them. At API boundaries, translate domain exceptions into HTTP exceptions or the framework's boundary type.
For Ruff TRY301 (raise-within-try), first restructure production code so the raise does not happen inside the try block. If the surrounding control flow really needs to raise inside try, ignore the raise line with # noqa: TRY301. Do not create a passthrough or one-line helper that only raises an error just to satisfy the rule.
Name caught exceptions e, unless that would shadow an existing name in the same scope.
Logging
Use structured logging with keyword arguments. Do not interpolate values into messages with %s or f-strings when they should be searchable context.
Bind stable context in the callee when multiple log lines share the same attributes. This keeps each log call focused on the event, not repeated context.
Datetimes
Always use timezone-aware datetimes. Prefer datetime.now(UTC) from the stdlib. Do not create new naive datetimes unless working with an API that explicitly requires naive values, and document the boundary conversion.
Ordering
At module scope, define private functions before public functions. Within those groups, put called helpers before callers so readers encounter building blocks before orchestration.
Inside classes, use the public reading order instead: callers before called private helpers. This keeps the public behavior near the top of the class and implementation details below it.
For dataclasses, Pydantic models, and similar declarative structures, sort fields alphabetically unless semantic grouping is intentionally clearer. If a non-alphabetical order could look accidental, add a brief comment.
Classes
Regular classes should not declare instance attributes in the class body. Put instance attributes in __init__ and annotate every assignment explicitly, even when the type is obvious from the constructor parameter.
This rule does not apply to dataclasses, Pydantic models, attrs classes, or other declarative class systems where class-body fields are the API.
Tests
Use snektest with @test(). The decorated function name is the test name, so it may omit test_ even though test files still use test_*.py.
Tests must verify one behavior at a time. Do not write “full surface”, “end-to-end everything”, or umbrella tests that combine multiple independent behaviors like insert + select result shapes + update + delete in one test. Split them into focused tests with names that state the single behavior under test.
Setup may use supporting operations, but assertions should target one behavior. For example, a select test may insert records as setup, but the assertions should verify only the selected result-shape behavior. If a test has unrelated assertion groups or naturally has two independent assertion branches, split it into focused tests with names that describe each behavior.
Tests must not mix setup, behavior under test, and assertions in the same logical block. Use fixtures or clearly separated helper setup when setup is non-trivial, especially for database state. The test body should make the behavior under test obvious.
Avoid doing setup inserts, the mutation under test, and result fetching/assertion all inside one transaction or block unless the transaction boundary itself is the behavior under test. Prefer this shape:
database = await load_fixture(database_with_seeded_users())
async with database.transaction() as tx:
await tx.execute(update(User).set(...).where(...))
async with database.transaction() as tx:
rows = await tx.fetch_all(select(...).all())
assert_eq(rows, expected)Avoid tests that exercise unrelated runtimes, backends, adapters, or policies in the same test. For example, SQLite runtime behavior and MariaDB runtime behavior should be separate tests.
Prefer separate tests for result-shape behavior, mutation behavior, error behavior, lifecycle behavior, and backend policy behavior.
Keep test names short and descriptive. Prefer names that state the single behavior under test, not umbrella names like covers X, Y, and Z. If a test name contains words like “full”, “surface”, “and”, or lists multiple verbs, challenge whether it should be multiple tests. Use the test function docstring for the case description or scenario details.
Fixture loading should happen at the top of the test body, immediately after the docstring and before local classes, setup logic, or assertions. Use fixtures to create external resources and seed prerequisite state. Mid-test load_fixture(...) is a smell: either move fixture acquisition to the top, or split the test so each test has its own clear setup and behavior.
When a class or module exists only to exercise behavior under test, define it inside the test function when practical. External fixtures should be acquired first so the test's dependencies are visible immediately.
Use test databases for database behavior and fake services for external systems. Clean up test data after tests.
Do not mark TDD phases with comments like RED or GREEN.
If a test violates Ruff TRY301 (raise-within-try), ignore it on the same line with # noqa: TRY301. Do not extract a one-line helper whose only behavior is raising the error.
Avoid cast() in tests even more strongly than in library code. It usually means the production code is hard to test or the test is reaching through the wrong interface.
Comments and suppressions
Do not write comments that merely narrate the next line of code. Comments should explain non-obvious constraints, tradeoffs, ordering, invariants, or history that affects the code.
Use suppression comments sparingly and scope them to the smallest possible surface. Most suppressions must include a short reason inline or in the comment immediately above.
TODO levels:
# TODO:can be merged tomain.# FIXME:can be committed on a branch, but should not merge tomain.# XXX:should be fixed before committing.
Function size, helper locality, and abstractions
There is no default preference for short functions. Prefer deep modules: narrow interfaces with substantial implementation behind them.
Avoid passthrough, one-line, and single-use functions unless they clarify genuinely complex logic. Never add a function whose only purpose is to move a single expression, raise statement, or lint violation somewhere else. A function may handle multiple related responsibilities when that keeps behavior local and easier to understand.
Keep helpers local to their scope. If a private helper is only used by one class, make it a private method or staticmethod on that class. If it is only used by one method, define it inside that method or inline it.
Locality of behavior and DRY often conflict. Prefer locality when removing repetition would scatter the behavior or create a premature abstraction.
Do not create an abstraction until there are at least two implementations.
Pydantic
Use model_config as a dictionary. It doesn't need to be typed, as the pydantic model already has a type annotation.