refactor-python
Refactor Python
Purpose
Refactor Python code safely and idiomatically while preserving behavior. Optimize for readable code, simple module boundaries, explicit contracts, useful tests, and maintainable Python. Avoid importing patterns from other ecosystems when simple functions, modules, dataclasses, protocols, or standard-library tools fit better.
Use this skill for cleanup, simplification, renaming, decomposition, dependency untangling, test characterization, typing improvements, packaging cleanup, or async/data-model refactors. Do not treat a rewrite, redesign, feature, or performance project as a refactor unless the user explicitly asks for that scope.
Non-Negotiables
- Preserve behavior. Keep public APIs, CLI flags, exceptions, log messages, file formats, ordering, database schemas, and side effects compatible unless the user approves a breaking change.
- Work from evidence. Read code, tests, config, docs, and call sites before changing design.
- Make one coherent change at a time. Prefer small, reviewable diffs over bundled redesigns.
- Protect risky behavior. Run existing checks; add characterization tests when coverage is missing and the refactor is not trivial.
- Follow the project. Match its Python version, formatter, linter, type checker, test style, package layout, and dependency manager.
- Avoid architecture astronautics. Add abstractions only when duplication, volatility, or dependency direction justifies them.
- Do not hide behavior changes inside cleanup. If you discover a bug, report it or fix it in a separate, explicit change.
First Inspection Checklist
Before editing, identify:
- Tooling/config:
pyproject.toml,uv.lock,.python-version,setup.cfg,tox.ini,noxfile.py,pytest.ini,.pre-commit-config.yaml, CI, or scripts. - Working tree and recent changes:
git status,git diff, and relevant recent tests when available. - Supported Python versions from
requires-python, lockfiles, CI, docs, or runtime constraints. - Test/lint/type commands and whether the repo uses
pytest,unittest,tox,nox,ruff,black,isort,mypy,pyright,basedpyright, orpylint. - Public contracts: documented imports,
__init__.py,__all__, entry points, CLIs, plugin hooks, serialized data, exceptions, and log output. - Call sites before renaming, moving, or changing signatures.
- Dependency manager. If the project uses
uv, use that workflow rather than manual virtualenv orpipcommands.
Safe Workflow
- Discover baseline. Read relevant code/config and run a focused existing check if practical. Record pre-existing failures.
- Characterize when needed. Add focused tests around current behavior before moving parsing, serialization, CLI behavior, async logic, or bug-prone code.
- Refactor one seam. Rename, extract, inline, move, or simplify one coherent concept.
- Verify. Run the narrowest useful tests/lint/type checks, expanding scope only as warranted.
- Review Python hazards. Inspect the diff for accidental behavior changes; check imports/cycles, public contracts, mutable defaults, exception behavior, async cancellation, encodings, paths, timezone/money precision, and dependency metadata.
- Report clearly. State files changed, checks run, compatibility notes, and remaining risk.
Verification Commands
Use project-defined commands first. Common focused commands:
python -m pytest path/to/test_file.py -q
python -m pytest -q
python -m ruff check .
python -m ruff format --check .
python -m mypy package_or_module
python -m pyright
For uv projects, prefer the locked environment:
uv run pytest path/to/test_file.py -q
uv run ruff check .
uv run ruff format --check .
uv run mypy package_or_module
uv run pyright
uv sync --locked
uv lock --check
Do not run expensive full suites casually. If checks cannot run, explain what was inspected and what remains unverified.
uv Projects
Treat a repo as uv-managed when you see uv.lock, [tool.uv], [tool.uv.workspace], or docs/scripts using uv run, uv sync, uv add, or uv lock.
- Run tools with
uv run <command>. - Add/remove dependencies with
uv add,uv add --dev, anduv remove. - Use
uv syncto create/update the environment; useuv sync --lockedoruv lock --checkto verify without changing the lockfile. - Do not hand-edit
uv.lock. - Do not introduce Poetry, Pipenv,
requirements.txt, or manualpip installworkflows into a uv project unless the repo already uses them for a specific purpose. - Keep runtime dependencies in
project.dependenciesand dev-only tools in dependency groups. - When removing imports, remove the corresponding dependency only after checking all code, extras, docs, and entry points.
- Preserve
requires-python; do not add syntax or dependencies incompatible with it.
If uv is not already used, do not migrate to uv as part of a refactor unless asked.
Refactoring Moves
Use the smallest move that clarifies the code:
- Rename confusing modules, functions, variables, types, or concepts after checking call sites.
- Extract cohesive functions/methods/modules from large blocks; pass only required data.
- Inline indirection that no longer earns its name.
- Move behavior closer to the data or dependency it primarily uses.
- Separate pure logic from effects so I/O, network, database, time, randomness, and environment access sit at boundaries.
- Introduce dataclasses/enums/protocols/type aliases only when they clarify invariants or stabilize a boundary.
- Decompose conditionals with guard clauses, named predicates, or dispatch when that is clearer and supported by the Python version.
- Add compatibility re-exports when moving public names that callers may import from old paths.
Example compatibility shim:
# old_module.py
from .new_module import useful_function
__all__ = ["useful_function"]
Smells and Preferred Responses
| Smell | Prefer | Avoid |
|---|---|---|
| Long function | Extract named, cohesive steps | Splitting by arbitrary line count |
| Large class/module | Separate stable responsibilities | Many tiny anemic wrappers |
| Duplication | Extract the shared concept after confirming behavior matches | Abstracting coincidental similarity |
| Confusing names | Use domain terms from callers/tests/docs | Cute abbreviations or generic names |
| Long parameter list | Group cohesive values into a dataclass/config object | Giant untyped bags of unrelated data |
| Deep nesting | Guard clauses and named predicates | Clever control flow that hides branches |
| Primitive obsession | Enum, Literal, NewType, TypedDict, or dataclasses when they enforce meaning |
Heavy classes for every scalar |
| Hidden side effects | Make dependencies explicit at boundaries | Helpers that look pure but perform I/O |
| Circular imports | Move shared types/helpers lower; use if TYPE_CHECKING: |
Import-time hacks or monkeypatching |
| Dead private code | Remove it and any tests that only covered it | Deleting public behavior tests or leaving commented alternatives |
Python-Specific Guidance
Data and state
- Use mutable defaults safely:
Nonesentinels ordataclasses.field(default_factory=...). - Prefer dataclasses,
NamedTuple,TypedDict, or existing models for dict-shaped structured data; choose based on mutability and boundary needs. - Use
slots=Trueonly when supported and safe for public classes; it can affect dynamic attributes, inheritance, and pickling. - Avoid boilerplate getter/setter classes. Use plain attributes or properties with real invariants.
- Prefer composition, functions, or
typing.Protocolover deep inheritance unless runtime registration or shared implementation is needed.
Imports, modules, and packaging
- Avoid import-time side effects: expensive I/O, network calls, logging configuration, environment mutation, argument parsing, or display/database initialization.
- Move script bodies behind
main()and return exit codes:
def main() -> int:
...
return 0
if __name__ == "__main__":
raise SystemExit(main())
- Preserve documented import paths, entry points, and
__all__. - Keep CLI/framework adapters and infrastructure separate from domain logic where practical.
Resources and external effects
- Use context managers for files, locks, temporary directories, database sessions, and network clients.
- Prefer
pathlib.Pathfor new internal path handling unless project style or APIs favor strings. - Specify encodings for text files when behavior should be stable.
- Make time, randomness, clients, repositories, and configuration explicit dependencies when that improves testability.
Exceptions and errors
- Preserve public exception types/messages during refactors.
- Avoid bare
except:and broadexcept Exceptionunless intentionally isolating a boundary. - Chain exceptions when translating errors:
raise DomainError(...) from exc. - Do not swallow
asyncio.CancelledError; preserve cancellation behavior.
Typing
Add or improve types when they clarify contracts and the project supports typing.
- Prefer built-in generics (
list[str]) when supported andcollections.abcinterfaces (Iterable,Sequence,Mapping,Callable) for accepted inputs. - Use
Protocolfor structural boundaries,TypeAliasfor repeated complex types, andobjectinstead ofAnywhen callers must narrow. - Use
Self,override,assert_never,match,StrEnum, ordatetime.UTConly when the configured Python/type-checker version supports them, or when the project already uses compatible backports. - Avoid lying annotations, invasive typing rewrites, unnecessary
cast(), and weakening typed code withAny. - Separate runtime imports from type-only imports with
if TYPE_CHECKING:when needed to avoid cycles.
Async, precision, and performance-sensitive behavior
- Preserve sync/async public contracts.
- Do not call blocking I/O directly inside async functions; use async libraries or executors.
- Keep event loop creation at application boundaries.
- Use timezone-aware datetimes for real-world timestamps.
- Use
Decimalfor money/exact decimal calculations; do not replace it withfloatduring cleanup. - Do not optimize blindly. Measure first if performance is the goal or a hot path is being changed.
Testing Guidance
- Prefer focused tests that lock current behavior before changing internals.
- Use
pytestfixtures/parametrization,tmp_path,monkeypatch,capsys, andcaplogwhen they fit. - Use mocks sparingly; assert externally visible behavior rather than implementation details.
- Keep tests deterministic by controlling time, randomness, locale, filesystem, and network.
- Consider property tests for parsers, serializers, validators, and numeric invariants when already in use or clearly valuable.
Anti-Patterns to Avoid
- Refactoring by formatting the whole repository unless requested.
- Mixing unrelated cleanup with feature work or bug fixes.
- Adding design patterns because a conditional exists.
- Turning every function into a class or every scalar into a value object.
- Replacing readable loops with dense comprehensions.
- Introducing optional dependencies for small standard-library tasks.
- Moving code without checking imports, entry points, docs, and compatibility exports.
- Changing public exception/log/message/data behavior casually.
- Hiding mutable configuration in module globals.
Final Response Checklist
When reporting back, include:
- What refactoring was done and why.
- Files changed.
- Tests/checks run and outcomes.
- Compatibility notes for moved or public APIs.
- Follow-up work that should remain separate.
More from jwd83/skills
system-verilog-expert
Expert SystemVerilog guidance for RTL, interfaces, and verification-adjacent code with EDA-friendly design patterns. Use when designing, reviewing, or refactoring hardware-oriented SystemVerilog and when spotting HDL antipatterns that cause timing, lint, CDC, or tool-flow pain.
12simplify
Simplifies and refines code for clarity, consistency, and maintainability while preserving all functionality. Focuses on recently modified code unless instructed otherwise.
10grill-me
Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
9wiki-me
Build or extend a project wiki — a persistent, interlinked markdown knowledge base that the LLM incrementally maintains from your sources. Use when the user wants to start a wiki, add new sources, or synthesize notes into a living knowledge base rather than one-off RAG.
9godot-gdscript-patterns
Master Godot 4 GDScript patterns including signals, scenes, state machines, and optimization. Use when building Godot games, implementing game systems, or learning GDScript best practices.
9next-step
Resume an in-progress codebase from its actual current state, identify the next smallest defensible implementation step, make that change, and update tests, docs, or plans to match. Use when a rewrite, migration, refactor, feature, or cleanup is underway and you want the agent to move the work forward safely without inventing a whole new roadmap.
6