AI-Powered Code Refactoring System
Refactoring without AI hits a cost ceiling: a senior developer understands what needs to be done, but 80% of the work is mechanical rewriting. AI handles this mechanics, leaving humans responsible for architectural decisions and result review.
Types of Refactoring and Approaches
Structural refactoring (extract method, move class, rename) — highly amenable to automation, high AI accuracy.
Pattern refactoring (transition from callbacks to async/await, adding dependency injection) — requires context understanding, AI handles it well with proper prompting.
Architectural refactoring (monolith → microservices, God Object → SRP) — AI generates plan and draft, final decisions rest with humans.
Refactoring System
from anthropic import Anthropic
from pathlib import Path
import subprocess
import ast
from typing import Literal
client = Anthropic()
REFACTORING_TYPES = {
"extract_function": "Extract repeated code into separate functions",
"simplify_conditions": "Simplify complex conditional expressions",
"add_type_hints": "Add type annotations everywhere they're missing",
"async_migration": "Transform synchronous code to async/await",
"error_handling": "Add explicit error handling",
"dataclass_conversion": "Transform dict-based structures into dataclasses/Pydantic",
"dependency_injection": "Add dependency injection instead of global objects",
"remove_duplication": "Eliminate code duplication (DRY)",
}
class CodeRefactorer:
def refactor(
self,
source_code: str,
refactoring_type: str,
context: str = "",
preserve_interface: bool = True,
) -> dict:
"""Performs refactoring and returns diff"""
instructions = REFACTORING_TYPES.get(refactoring_type, refactoring_type)
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=8096,
system=f"""You are a senior developer performing Python code refactoring.
Refactoring task: {instructions}
Rules:
- {"Preserve public interface (function names, arguments, return types)" if preserve_interface else "Interface can be changed"}
- Do not change logic — only code structure
- If adding new helper functions — place them above main ones
- Preserve all existing comments and docstrings
- Add comment # REFACTORED: <brief description> where there were key changes""",
messages=[{
"role": "user",
"content": f"""Perform refactoring:
```python
{source_code}
{f"Project context:{chr(10)}{context}" if context else ""}
Return:
- Refactored code
- List of changes (bullet points)
- Potential risks if any
Response format:
<refactored code>
Changes:
- ...
Risks:
-
...""" }] )
text = response.content[0].text # Parse response refactored_code = "" if "```python" in text: refactored_code = text.split("```python")[1].split("```")[0].strip() changes = [] risks = [] if "**Changes:**" in text: changes_section = text.split("**Changes:**")[1].split("**Risks:**")[0] changes = [line.strip("- ").strip() for line in changes_section.splitlines() if line.strip().startswith("-")] if "**Risks:**" in text: risks_section = text.split("**Risks:**")[1] risks = [line.strip("- ").strip() for line in risks_section.splitlines() if line.strip().startswith("-")] return { "original": source_code, "refactored": refactored_code, "changes": changes, "risks": risks, }
### Async Migration — Real Refactoring Example
```python
def migrate_to_async(source_file: str) -> str:
"""Migrates synchronous code to async/await"""
source = Path(source_file).read_text()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=8096,
system="""Migrate Python code from synchronous to async/await.
Rules:
- requests → httpx.AsyncClient
- time.sleep(n) → asyncio.sleep(n)
- threading.Thread → asyncio.create_task
- queue.Queue → asyncio.Queue
- Add async/await to functions doing I/O
- Keep synchronous functions without I/O (pure computations)
- Replace for loops with asyncio.gather where functions are independent""",
messages=[{
"role": "user",
"content": f"Migrate to async/await:\n\n```python\n{source}\n```\n\nReturn code only."
}]
)
text = response.content[0].text
if "```python" in text:
return text.split("```python")[1].split("```")[0].strip()
return text
def add_type_hints(source_file: str) -> str:
"""Adds type annotations to functions"""
source = Path(source_file).read_text()
# Find functions without annotations
tree = ast.parse(source)
unannotated = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
has_annotations = (
any(arg.annotation for arg in node.args.args) or
node.returns is not None
)
if not has_annotations:
unannotated.append(node.name)
if not unannotated:
return source
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=8096,
messages=[{
"role": "user",
"content": f"""Add Python type annotations (PEP 484) to functions: {', '.join(unannotated)}
Rules:
- Use from __future__ import annotations for forward references
- For Optional use X | None (Python 3.10+)
- For collections: list[str], dict[str, int], tuple[int, ...]
- For unknown types use Any from typing
```python
{source}
Return full file with added annotations.""" }] )
text = response.content[0].text
if "```python" in text:
return text.split("```python")[1].split("```")[0].strip()
return text
### Refactoring with Test Insurance
```python
def safe_refactor(
source_file: str,
refactoring_type: str,
test_file: str = None,
) -> dict:
"""Performs refactoring only if tests pass"""
source = Path(source_file).read_text()
refactorer = CodeRefactorer()
# First run existing tests
if test_file and Path(test_file).exists():
result = subprocess.run(
["python", "-m", "pytest", test_file, "-v", "--tb=short"],
capture_output=True, text=True
)
if result.returncode != 0:
return {"success": False, "error": "Tests failing before refactoring", "test_output": result.stdout}
# Perform refactoring
refactoring = refactorer.refactor(source, refactoring_type)
# Create backup
backup_file = source_file + ".bak"
Path(backup_file).write_text(source)
# Write refactored code
Path(source_file).write_text(refactoring["refactored"])
# Run tests again
if test_file and Path(test_file).exists():
result = subprocess.run(
["python", "-m", "pytest", test_file, "-v", "--tb=short"],
capture_output=True, text=True
)
if result.returncode != 0:
# Roll back
Path(source_file).write_text(source)
return {
"success": False,
"error": "Tests failing after refactoring — rolled back",
"changes": refactoring["changes"],
"test_output": result.stdout,
}
return {
"success": True,
"changes": refactoring["changes"],
"risks": refactoring["risks"],
"backup": backup_file,
}
Practical Case: Django Monolith
Situation: Django project, 6 years of development, 45,000 lines. 3 problems: no type hints (new developers spend much time understanding types), lots of view-function duplication, 12 God Object classes.
Applied refactorings (over 3 weeks):
-
add_type_hintsfor allviews.pyfiles — automatic, 2 hours -
extract_functionfor view-functions > 50 lines — automatic + review, 1 week -
remove_duplicationin service layer — automatic, 3 days
Results:
- Type annotations coverage: 12% → 87%
- Average function length: 68 lines → 23 lines
- Code duplication (SonarQube metric): -43%
- New developer onboarding time: 3 weeks → 1.5 weeks
One God Object (OrderService, 1800 lines) required manual architectural decision — AI proposed 3 decomposition options, developers chose the optimal one.
Timeline
- Basic refactoring of one type for a file: 1–2 days
- safe_refactor system with test insurance: 3–5 days
- Batch refactoring of entire codebase: 2–3 weeks
- IDE integration as a command: 1 week







