| name | py-refactor |
| description | Use when refactoring Python code, cleaning up legacy codebases, optimizing performance, enforcing type safety, or improving clean architecture in a FastAPI backend |
Python Refactoring & Performance
Overview
Safe refactoring patterns, type safety enforcement, and performance optimization for Python/FastAPI backends.
Core principle: Never refactor without characterization tests. Never optimize without profiling. Always improve type coverage.
3-strikes rule: If the same refactoring approach fails 3 times, stop. The problem is architectural — escalate to system-design for a deeper review instead of thrashing.
Safe Refactoring Process
- Write characterization tests — capture current behavior
- Run mypy — baseline type errors
- Refactor — change structure, preserve behavior
- Verify — tests pass, mypy errors same or fewer
- Clean up — remove temporary tests if redundant
Architecture Enforcement
grep -r "from src.infrastructure\|from sqlalchemy\|from fastapi" src/domain/
grep -r "from src.infrastructure\|from sqlalchemy" src/application/
Common Refactoring Moves
| Smell | Move |
|---|
| Fat router | Extract use case class |
| ABC with single impl | Use Protocol instead |
| Inheritance hierarchy | Composition with Protocol |
| Sync blocking calls | async with asyncio.gather |
| Untyped dict passing | Pydantic model or dataclass |
| God class | Split into focused services |
Protocol Over ABC
from abc import ABC, abstractmethod
class UserRepository(ABC):
@abstractmethod
async def create(self, user: User) -> User: ...
from typing import Protocol
class UserRepository(Protocol):
async def create(self, user: User) -> User: ...
Async Optimization
users = await user_repo.list()
posts = await post_repo.list()
users, posts = await asyncio.gather(
user_repo.list(),
post_repo.list(),
)
SQLAlchemy Query Optimization
engine = create_async_engine(url, echo=True)
from sqlalchemy.orm import selectinload, joinedload
stmt = select(UserModel).options(selectinload(UserModel.posts))
stmt = select(PostModel).options(joinedload(PostModel.author))
Performance Profiling
py-spy record -o profile.svg --pid $(pgrep uvicorn)
python -m cProfile -o output.prof -m pytest tests/
mypy Strict Compliance
mypy src/ --strict 2>&1 | head -50
Legacy Code Rescue
When working with legacy Python code — untyped, untested, messy:
Step 1: Characterize Before Touching
def test_legacy_create_user_current_behavior():
"""Documents what legacy code ACTUALLY does — not what it should do."""
result = legacy_create_user({"email": "test@test.com"})
assert result["id"] is not None
assert result["email"] == "test@test.com"
Never refactor untested code. Add characterization tests first.
Step 2: Add Types Incrementally
Don't try to type the whole codebase at once. Start with boundaries:
async def create_user(body: CreateUserRequest) -> UserResponse: ...
@dataclass
class User:
id: UUID
email: str
name: str
class UserRepository(Protocol):
async def create(self, user: User) -> User: ...
mypy src/interfaces/ --strict
mypy src/domain/ --strict
mypy src/ --strict
Step 3: Identify the Worst Offenders
Prioritize by risk:
- Bare
except: or except Exception: — hiding real errors
- SQL string concatenation — SQL injection, fix immediately
- No input validation — user data flows unchecked into DB
- God files (500+ lines) — split by responsibility
- Sync blocking in async —
time.sleep(), sync DB calls in async handlers
- Untyped dict passing — replace with Pydantic models or dataclasses
Step 4: Strangler Fig Pattern
class UserService(Protocol):
async def create(self, data: CreateUserInput) -> User: ...
class LegacyUserService:
async def create(self, data: CreateUserInput) -> User:
...
class CleanUserService:
def __init__(self, repo: UserRepository) -> None:
self._repo = repo
async def create(self, data: CreateUserInput) -> User:
user = User.from_input(data)
return await self._repo.create(user)
def get_user_service() -> UserService:
return CleanUserService(repo=PostgresUserRepository(session))
Step 5: Fix Dangerous Patterns
try:
result = do_something()
except:
pass
try:
result = do_something()
except ValueError as e:
logger.warning("Invalid input", error=str(e))
raise
except DatabaseError as e:
logger.error("Database failure", error=str(e))
raise
Step 6: Remove Dead Code
uvx vulture src/
ruff check --select F401 .
Delete it. Git has history.
Chains
- REQUIRED: Use
superpowers:systematic-debugging for performance investigation
- REQUIRED: Write characterization tests before any refactoring — no exceptions
- REQUIRED: Update CLAUDE.md with discovered gotchas and conventions (
claude-md)
- Legacy codebases: Run
fullstack-healthcheck first to prioritize what to fix