ワンクリックで
usethis-python-code
Guidelines for Python code design decisions such as when to share vs. duplicate code
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Guidelines for Python code design decisions such as when to share vs. duplicate code
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
General guidelines for writing tests in the usethis project, including test class organization
Create a lesson from a development difficulty, covering root cause analysis, principle generalisation, and filing as a GitHub issue
Write bespoke prek hooks as reusable Python scripts for custom checks
Check whether docs/config-files.txt (the machine-readable export) is in-sync with the per-tool tables in docs/about/config-files.md
Enforce version bumping, scope checking, and content quality guidelines when modifying SKILL.md files
Modify Python code (e.g. refactor, add new code, or delete code)
| name | usethis-python-code |
| description | Guidelines for Python code design decisions such as when to share vs. duplicate code |
| compatibility | usethis, Python |
| license | MIT |
| metadata | {"version":"1.11"} |
Use the usethis-python-code-modify skill when making code changes. This skill covers design decisions that inform those changes.
When referencing code names (functions, classes, modules, parameters, etc.) in docstrings, always use single backticks (`), not double backticks (``). This applies to all identifiers mentioned in docstring text.
# Good: single backticks
def add_dep(name: str) -> None:
"""Add a dependency using `uv add`."""
# Bad: double backticks (RST style)
def add_dep(name: str) -> None:
"""Add a dependency using ``uv add``."""
The project uses Markdown-compatible formatting for docstrings. Single backticks are the Markdown standard for inline code, and the project's export hooks normalize double backticks to single backticks. Using single backticks from the start avoids this normalization and keeps docstrings consistent with their rendered output. The check-docstring-substrings prek hook enforces this automatically.
When writing new code or replacing a dependency with custom code, prefer a single shared implementation over duplicating logic across modules. Only introduce duplication when there is a concrete, present-day reason to do so.
Duplication is acceptable when the implementations genuinely differ in behavior or intent, even if they look superficially similar. Ask:
If the answer to any of these is yes, duplication may be the right choice. If the answer to all is no, share the code.
Before adding a new function to a module, consider at least two candidate locations. Placement should be driven by the level of abstraction the function belongs to, not by proximity to the call site.
usethis-qa-import-linter skill). If it would, introduce a new shared lower-level module that both the chosen module and its callers can depend on, and declare it in the relevant contract.A function that extends a lower-level utility module with follow-up logic belongs in that utility module, not in the higher-level module that happens to call it. Placing logic too high in the stack couples the lower-level module's behavior to the higher-level module's concerns.
If a function processes the output of a file-reading utility and belongs conceptually to the file layer, place it in the file module — even if the only current caller is an integration module. The integration module imports from the file module, so the direction of dependency is already correct.
When code accesses deeply nested attributes of an object (e.g. result.solution.root), it is reaching through multiple abstraction layers. This is a sign that the logic is at the wrong level — it belongs in a lower layer, closer to the objects being accessed.
obj.inner.deep_attr), stop and consider which layer owns that information.If you are accessing low-level attributes via deep nesting, the logic is in the wrong place. Always move it into a lower layer — either the layer that owns the deeply nested object, or an interface on the intermediate object.
# Bad: higher layer reaches through result -> solution -> root and operates on it
result = adder.add()
flat = result.solution.root
idx = flat.index(step_name)
predecessor = flat[idx - 1] if idx > 0 else None
# Good: lower layer exposes a method that encapsulates the logic
result = adder.add()
predecessor = result.get_predecessor(step_name)
In the bad example, the caller knows that solution is a Series, that Series has a root list, and how to find a predecessor by index. In the good example, the result object owns that logic and the caller only interacts with one level of abstraction.
flat = result.solution.root does not fix the problem — it only hides the nesting in a local variable while the caller still depends on the internal structure.assert isinstance(item, str) after accessing a nested attribute, the logic almost certainly belongs in the layer that produces the object, where the type is already known.self.solution.root is not enough. The goal is to move the logic that uses the low-level data into the lower layer, not just to add an accessor.When code needs to preserve and restore state around a block of operations (e.g. backing up a file before a subprocess and restoring it afterwards), always use a @contextmanager instead of separate setup and teardown helper functions with a try/finally block.
@contextmanager generator function that yields between the setup and teardown.try/finally inside the context manager to guarantee cleanup runs even if the body raises.A context manager encapsulates the setup/teardown lifecycle into a single construct, making the calling code cleaner and eliminating the risk of forgetting the finally block or mismatching the paired calls. It also makes the intent clearer at the call site.
# Bad: separate helpers require the caller to manage the lifecycle
def _backup(path: Path) -> Path:
...
def _restore(path: Path, backup: Path) -> None:
...
backup = _backup(lock_path)
try:
run_subprocess()
finally:
_restore(lock_path, backup)
# Good: context manager encapsulates the lifecycle
@contextmanager
def _preserved(path: Path) -> Generator[None, None, None]:
with tempfile.TemporaryDirectory() as tmp:
backup = shutil.copy2(path, tmp)
try:
yield
finally:
shutil.copy2(backup, path)
with _preserved(lock_path):
run_subprocess()
try/finally correctly. A context manager removes this burden.finally in the caller. Without a context manager, it is easy to forget the finally block, leaving state unrestored if an exception occurs. A context manager guarantees cleanup.yield without try/finally in a @contextmanager. With @contextmanager, cleanup code placed after yield does NOT run if an exception is raised inside the with block. The exception is re-raised at the yield point, causing the generator to terminate without reaching the cleanup code. Always wrap yield in try/finally to guarantee cleanup runs on both normal exit and exceptions.Within a module, place caller functions above their callees. The module should read top-to-bottom like a newspaper: the most important, high-level logic appears first, and helper details appear further down.
A reader scanning a module should encounter each function before its helpers, never the other way around. If a helper appears above its caller, the reader is confronted with implementation details before knowing what they are for.
# Bad: helper placed above its caller
def _format_version(v: str) -> str:
return v.strip()
def get_version() -> str:
"""Return the formatted version string."""
return _format_version(read_version_file())
# Good: caller appears first, helper appears below
def get_version() -> str:
"""Return the formatted version string."""
return _format_version(read_version_file())
def _format_version(v: str) -> str:
return v.strip()
When an outer if-else branches on an enum (or similar value), and a shared guard condition or action applies to a group of those branches, check the guard once at the outer level using a combined membership test before branching on the individual values.
If the same string literal or logic block appears in two sibling branches of an if-else, that is a signal to restructure: hoist the shared condition or action to the outer level, and move the distinguishing logic to the inner level.
if value in (A, B):.else: assert_never(...) guard is still present at every level where the value is dispatched.# Bad: same tick_print duplicated in two sibling branches
if backend is BackendEnum.uv:
tick_print("Doing work in 'pyproject.toml'.")
do_uv_work()
elif backend is BackendEnum.poetry:
tick_print("Doing work in 'pyproject.toml'.") # identical
do_poetry_work()
elif backend is BackendEnum.none:
instruct_print("Do the work manually.")
else:
assert_never(backend)
# Good: shared message hoisted; distinguishing logic nested inside
if backend in (BackendEnum.uv, BackendEnum.poetry):
tick_print("Doing work in 'pyproject.toml'.")
if backend is BackendEnum.uv:
do_uv_work()
elif backend is BackendEnum.poetry:
do_poetry_work()
else:
assert_never(backend)
elif backend is BackendEnum.none:
instruct_print("Do the work manually.")
else:
assert_never(backend)
assert_never. After introducing the combined membership check, the inner if-elif still needs its own else: assert_never(backend) to preserve exhaustiveness checking.@finalWhen a property or method derives its return value entirely from another member that is the designated override point, mark it @final to prevent subclasses from overriding it with independent logic. This applies equally to properties that project a source-of-truth attribute and to methods that compute their result from a separate method that subclasses are expected to override.
A @final marker signals that the member is not a hook designed for extension — it is a thin derivation of its underlying source. @final is enforced by type checkers and IDEs at the point of violation; prose documentation (e.g. "do not override this method") is merely advisory and silently ignored. Always prefer @final over prose to communicate non-override constraints.
@final above the @property decorator.@final to prevent subclasses from bypassing that override point.@final member in a subclass; type checkers will flag this as an error.@final. Adding "do not override this method" or "tools should override X instead" to a docstring does not prevent overrides. Use @final to make the constraint machine-checkable.@final on new derived members. Whenever you add a property or method that simply forwards to or derives from a single source, mark it @final immediately to prevent the same mistake in the future.When a private helper function performs file I/O or another expensive operation and may be called more than once during a single high-level operation, decorate it with @functools.cache to avoid redundant work.
Apply @functools.cache to a private helper when all of the following are true:
Do not cache functions that write files, mutate state, or produce side effects.
import functools to the module if not already present.@functools.cache.clear_functools_caches autouse fixture in tests/conftest.py (see usethis-python-test for details).@functools.cache is only appropriate for pure, read-only operations. Functions that write files or change state should never be cached.