Code migration is one of the most time-intensive engineering tasks — and one where AI provides the clearest ROI. Migrating 200 files from one pattern to another is mechanical work that takes weeks manually. With AI-assisted automation, the same migration takes hours. This guide covers practical approaches to three migration types: framework/library upgrades, language ports, and API breaking changes.
Migration Type 1: Library Upgrade with Breaking Changes
Example: Migrating from React Router v5 to v6 (useHistory to useNavigate, Switch to Routes, etc.)
import anthropic
from pathlib import Path
client = anthropic.Anthropic()
MIGRATION_PROMPT = """You are migrating React Router v5 code to v6.
Apply ONLY these transformations:
1. Replace useHistory() with useNavigate()
2. Replace history.push('/path') with navigate('/path')
3. Replace history.replace('/path') with navigate('/path', { replace: true })
4. Replace <Switch> with <Routes>
5. Add element prop to <Route>: <Route path="x" component={Y}/> -> <Route path="x" element={<Y />}/>
6. Remove <Redirect> and replace with <Navigate to="..."/>
Rules:
- Only modify React Router imports and usage
- Do not change component logic
- If the code has no React Router usage, return it unchanged
- Preserve all formatting and whitespace
Return ONLY the transformed code, no explanation."""
def migrate_file(filepath: str) -> tuple[str, bool]:
with open(filepath) as f:
original = f.read()
if "react-router" not in original:
return original, False
response = client.messages.create(
model="claude-haiku-3-5",
max_tokens=4096,
messages=[
{
"role": "user",
"content": f"{MIGRATION_PROMPT}\n\nFile to migrate:\n```jsx\n{original}\n```"
}
]
)
migrated = response.content[0].text
if migrated.startswith("```"):
migrated = "\n".join(migrated.split("\n")[1:-1])
changed = migrated.strip() != original.strip()
return migrated, changed
src_dir = Path("./src")
files = list(src_dir.rglob("*.tsx")) + list(src_dir.rglob("*.jsx"))
results = {"changed": [], "unchanged": [], "errors": []}
for filepath in files:
try:
migrated_content, was_changed = migrate_file(str(filepath))
if was_changed:
staging_path = Path("./migration-staging") / filepath.relative_to("./src")
staging_path.parent.mkdir(parents=True, exist_ok=True)
staging_path.write_text(migrated_content)
results["changed"].append(str(filepath))
else:
results["unchanged"].append(str(filepath))
except Exception as e:
results["errors"].append({"file": str(filepath), "error": str(e)})
print(f"Changed: {len(results['changed'])} files")
Always stage, validate, then apply. Never write AI-migrated code directly over source files without a validation step.
Migration Type 2: Language Port (Python 2 to Python 3)
For large codebases, use the Anthropic Batch API to process hundreds of files concurrently at 50% cost:
import anthropic
import json
from pathlib import Path
client = anthropic.Anthropic()
PYTHON_23_MIGRATION_PROMPT = """Migrate this Python 2 code to Python 3.8+.
Apply ALL of these transformations:
1. print statements -> print() function calls
2. unicode() -> str(), basestring -> str
3. except ExceptionType, e: -> except ExceptionType as e:
4. dict.has_key(k) -> k in dict
5. dict.iteritems() -> dict.items(), dict.itervalues() -> dict.values()
6. xrange() -> range()
7. raw_input() -> input()
8. Remove __future__ imports
Return ONLY the migrated Python 3 code."""
python_files = list(Path("./legacy_app").rglob("*.py"))
batch_requests = []
for i, filepath in enumerate(python_files):
with open(filepath, encoding="latin-1") as f:
content = f.read()
batch_requests.append({
"custom_id": f"file-{i}",
"params": {
"model": "claude-haiku-3-5",
"max_tokens": 8192,
"messages": [
{"role": "user", "content": f"{PYTHON_23_MIGRATION_PROMPT}\n\n```python\n{content}\n```"}
],
}
})
batch = client.beta.messages.batches.create(requests=batch_requests)
print(f"Batch submitted: {batch.id}")
print(f"Processing {len(batch_requests)} files at 50% API cost")
Migration Type 3: Internal API Breaking Changes
When you rename a method or change a signature, every call site needs updating.
def build_api_migration_prompt(old_signature: str, new_signature: str, changelog: str) -> str:
return f"""You are updating call sites to use a new API.
OLD API:
{old_signature}
NEW API:
{new_signature}
MIGRATION NOTES:
{changelog}
Transform any code that calls the old API to use the new API.
If the file doesn't use this API, return it unchanged.
Preserve all formatting. Return only the transformed code."""
OLD_API = """
def send_notification(user_id: int, message: str, channel: str = "email") -> bool
"""
NEW_API = """
async def notify_user(
message: str,
user_id: int,
channels: list[str] = ["email"],
priority: str = "normal"
) -> NotificationResult
"""
CHANGELOG = """
- Function renamed from send_notification to notify_user
- Now async - all callers must await it
- Parameters reordered: message is now first, user_id second
- 'channel' renamed to 'channels' and is now a list
- Return type changed from bool to NotificationResult
"""
import subprocess
result = subprocess.run(
["grep", "-rl", "send_notification", "./app"],
capture_output=True, text=True
)
affected_files = result.stdout.strip().split("\n")
Validation After AI Migration
import ast
import subprocess
def validate_python_syntax(filepath: str) -> bool:
try:
with open(filepath) as f:
ast.parse(f.read())
return True
except SyntaxError as e:
print(f"Syntax error in {filepath}: {e}")
return False
def validate_typescript(directory: str) -> list[str]:
result = subprocess.run(
["npx", "tsc", "--noEmit", "--project", f"{directory}/tsconfig.json"],
capture_output=True, text=True
)
if result.returncode != 0:
return result.stdout.split("\n")
return []
The standard migration workflow:
- Run migration script into staging directory
- Syntax validation (fast, catches obvious failures)
- Type check (catches semantic issues)
- Run existing test suite (catches behavioral regressions)
- Human review of files flagged by any check
- Apply staging to source
What AI Migration Gets Wrong
Multi-line destructuring: An AI might correctly identify useHistory() but miss:
const { push, replace } = useHistory();
// Needs: const navigate = useNavigate();
// Then: push('/path') -> navigate('/path')
Dynamic usage: const method = condition ? history.push : history.replace — transformation depends on runtime logic.
Test files: Often use the old API in mock setups with different patterns than production code.
Flag these categories for manual review rather than trusting automated migration.
Diff Review Workflow
The biggest risk with AI migration is silent correctness issues — code that passes the type checker but behaves differently. Build a diff review step into every migration:
import difflib
def generate_migration_diff(original: str, migrated: str, filepath: str) -> str:
diff = difflib.unified_diff(
original.splitlines(keepends=True),
migrated.splitlines(keepends=True),
fromfile=f"a/{filepath}",
tofile=f"b/{filepath}",
n=3
)
return "".join(diff)
# Write all diffs to a single review file
with open("migration-review.diff", "w") as review:
for filepath in results["changed"]:
original = open(filepath).read()
migrated = open(f"migration-staging/{filepath}").read()
review.write(generate_migration_diff(original, migrated, filepath))
review.write("\n")
Run git diff --stat migration-review.diff or open it in a tool like delta to review all changes before applying. High-confidence mechanical changes (print statements, import renames) need less scrutiny. Low-confidence changes (async addition, parameter reordering) need a human read.
Sizing the Migration
Rule of thumb for estimating AI migration effort:
| Files | Manual Estimate | AI-Assisted Estimate | Notes |
|---|---|---|---|
| < 20 files | 1-2 days | 2-4 hours | Manual review still faster to set up |
| 20-100 files | 1-2 weeks | 4-8 hours | Clear ROI for AI assistance |
| 100-500 files | 4-8 weeks | 1-2 days | Batch API essential |
| 500+ files | Quarter-long project | 3-5 days | Requires phased rollout |
The time savings grow with scale. For fewer than 20 files, writing a migration prompt and validation script may take longer than just doing it manually.
Related Articles
- AI Code Completion for Java Jakarta EE Migration from Javax
- Best AI Tools for Code Migration Between Languages 2026
- Best AI Tools for Code Migration Python 2
- AI Tools for Automated API Documentation from Code Comments
- Best AI Tools for Automated Code Review 2026
Built by theluckystrike — More at zovo.one