| name | refactor-code |
| description | Modernize legacy Python code with type hints and efficient patterns. Use when asked to refactor, modernize, or update Python code. |
Python Refactoring Specialist
Prune with the cleanup-code agent before refactoring.
Refactor code that should exist — not code that should be deleted.
Cleanup first, refactor second.
Transform legacy Python code into maintainable, efficient implementations.
Refactoring Process
1. Assessment
- Check the project's minimum supported Python version before applying any version-gated pattern. Look in
pyproject.toml, setup.cfg, .python-version, or CI config (e.g., python-version matrix in .github/workflows/). Only apply features available at or below that floor — do not use match statements on a 3.9 codebase, tomllib below 3.11, etc.
- Inventory legacy patterns using the Modernization Checklist below as your scanning tool. For each item, flag it as: applies and safe, applies but needs version check, or not present.
- Prioritize refactoring based on impact and risk.
- Check for existing tests — ensure they exist and pass before refactoring anything.
2. When to Skip a Modernization
Skip updates if:
- The code is already clear and short.
- The change is version-gated and the project's minimum Python version is too low.
- The file is generated, a one-off migration script, or unlikely to be maintained.
- The abstraction (e.g., a dataclass for a two-field struct) is heavier than the code it replaces.
Modernize to reduce noise, improve safety, or clarify intent.
3. Safe Refactoring Steps
- Run existing tests to establish a baseline.
- For large codebases with many mechanical changes (f-strings, pathlib, import ordering), run automated tools first —
pyupgrade, ruff --fix, or python-modernize — for bulk transformations. Reserve manual refactoring for structural changes (dataclasses, match, async patterns, type hints).
- Apply one refactoring pattern at a time.
- Run tests after each change.
- Verify functionality remains identical.
4. Modernization Checklist
Section 1: Type System
Section 2: Syntax Modernization
Section 3: Structural Improvements
Async (if applicable)
Imports
Quality Assurance
For detailed tool commands, see CLAUDE.md if present in the project root; otherwise use the defaults below.
Before Refactoring
Run type checking, linting, and tests to establish baseline (from project root; use uv run when pyproject.toml exists):
uv run mypy src/
uv run ruff check .
uv run pytest --tb=short
uv run pytest --cov=src --cov-report=term-missing
If the project has no uv/pyproject.toml, use commands from the project README instead.
Record the baseline pass/fail counts. Flag existing test failures; do not treat them as regressions.
After Refactoring
Verify refactored code passes all checks:
- Type checking (no new mypy errors)
- Linting (no new lint violations)
- Unit tests (same or better pass rate as baseline)
- Coverage must match the baseline
Common Refactoring Scenarios
Scenario 1: Migrate to f-strings
msg = "Host %s unreachable after %d retries" % (host, retries)
msg = "Host {} unreachable after {} retries".format(host, retries)
msg = f"Host {host} unreachable after {retries} retries"
msg = f"{'Interface':<20} {'Status':>10}"
Scenario 2: Migrate to dataclasses
class DeviceInfo:
def __init__(self, hostname, ip, platform):
self.hostname = hostname
self.ip = ip
self.platform = platform
def __repr__(self):
return f"DeviceInfo({self.hostname}, {self.ip}, {self.platform})"
from dataclasses import dataclass
@dataclass
class DeviceInfo:
hostname: str
ip: str
platform: str
Scenario 3: Migrate to pathlib
import os
config_path = os.path.join(base_dir, "config", "devices.yaml")
if os.path.exists(config_path):
with open(config_path) as f:
...
from pathlib import Path
config_path = Path(base_dir) / "config" / "devices.yaml"
if config_path.exists():
with config_path.open() as f:
...
Scenario 4: Migrate to match statements (Python 3.10+ only)
if platform == "ios":
driver = IOSDriver()
elif platform == "eos":
driver = EOSDriver()
elif platform == "nxos":
driver = NXOSDriver()
else:
raise ValueError(f"Unknown platform: {platform}")
match platform:
case "ios":
driver = IOSDriver()
case "eos":
driver = EOSDriver()
case "nxos":
driver = NXOSDriver()
case _:
raise ValueError(f"Unknown platform: {platform}")
Scenario 5: Replace print with logging
print(f"Connecting to {host}...")
print(f"ERROR: timeout on {host}")
import logging
logger = logging.getLogger(__name__)
logger.debug("Connecting to %s", host)
logger.error("Timeout on %s", host)
Why %-style args in logging, not f-strings? The logging module uses lazy formatting — the string is only interpolated if the log level is actually active. With f-strings, the interpolation happens unconditionally at the call site, even if the message is never emitted. Use %-style positional args ("msg %s", value) in all logger.* calls.
Modernize legacy Python code after pruning with the cleanup-code agent.
See Also
rules/python-style.md — Toolchain, typing, security scans
cleanup-code agent — Run this first to prune dead code before refactoring
review-code skill — For final gate review after refactoring