I translated 3,000 lines of Python to TypeScript using Claude in three days
← Back
April 4, 2026Claude7 min read

I translated 3,000 lines of Python to TypeScript using Claude in three days

Published April 4, 20267 min read

We needed to migrate a Python data processing service to TypeScript so it could run in our Node.js monorepo. The service was 3,000 lines across 18 files. I estimated two weeks. Using Claude with a specific translation workflow, I finished the working translation in three days and spent another two days on testing and cleanup. Here is the workflow and the gotchas I learned.

The translation system prompt

text
Translation rules:
1. Use TypeScript idioms — don't write "Python in TypeScript"
2. Python dict -> TypeScript Record or interface
3. Python Optional[T] -> T | null (not T | undefined unless optional param)
4. Python dataclass -> TypeScript interface
5. Python asyncio -> Promise + async/await
6. Python context managers -> try/finally
7. Python f-strings -> template literals
8. Preserve all docstrings as JSDoc comments
9. Add explicit TypeScript interfaces for all major data shapes
10. Use const by default, let only when reassigned

File-by-file translation workflow

python
import anthropic
from pathlib import Path

client = anthropic.Anthropic()

def translate_file(python_source: str, file_name: str) -> str:
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=8000,
        system="You are translating Python to TypeScript. Use TypeScript idioms. Return ONLY code, no explanation.",
        messages=[{
            "role": "user",
            "content": f"Translate to TypeScript. File: {file_name}

{python_source}"
        }]
    )
    text = response.content[0].text
    # Strip markdown fences if present
    if text.startswith("```"):
        lines = text.split('
')
        text = '
'.join(lines[1:-1])
    return text

def translate_project(python_dir: str, ts_dir: str) -> None:
    for py_file in sorted(Path(python_dir).glob("**/*.py")):
        ts_file = Path(ts_dir) / py_file.relative_to(python_dir).with_suffix('.ts')
        ts_file.parent.mkdir(parents=True, exist_ok=True)
        print(f"Translating {py_file.name}...")
        ts_file.write_text(translate_file(py_file.read_text(), py_file.name))

The idioms that need manual fixing

After translating 18 files, these patterns consistently needed manual correction:

typescript
// Python: dict.setdefault(key, []).append(value)
// Claude produces (wrong — creates new array each time):
const arr = map[key] ?? [];
arr.push(value);
map[key] = arr;

// Correct TypeScript:
if (!map[key]) map[key] = [];
map[key].push(value);

// Python: sorted(items, key=lambda x: x.score, reverse=True)
// Claude produces (correct but verbose):
items.sort((a, b) => b.score - a.score);
// Better:
const sorted = [...items].sort((a, b) => b.score - a.score); // non-mutating

// Python: Counter(words).most_common(10)
// Claude invents a Counter class that may not match Map semantics
// Better to verify and use native Map:
const counts = new Map();
for (const word of words) {
  counts.set(word, (counts.get(word) ?? 0) + 1);
}
const topTen = [...counts.entries()]
  .sort(([, a], [, b]) => b - a)
  .slice(0, 10);

Verification strategy

After translation, I used this approach to verify correctness:

  1. Port the Python tests — same workflow, translate each test file
  2. Run both versions on the same input — pipe identical JSON through Python and TypeScript versions and diff the output
  3. Fix discrepancies one at a time — paste the differing output back to Claude with both implementations and ask what is different
bash
# Compare outputs side by side
echo '{"input": "test data"}' | python3 service.py > /tmp/py_output.json
echo '{"input": "test data"}' | node dist/service.js > /tmp/ts_output.json
diff /tmp/py_output.json /tmp/ts_output.json

The actual time breakdown

Day 1: Translate 18 files (3 hours) + TypeScript compilation errors (2 hours)
Day 2: Port test suite + fix output discrepancies (8 hours)
Day 3: Edge cases + integration testing + cleanup (8 hours)
Day 4-5: Load testing, performance comparison, documentation

The biggest time sink was not translation — it was the Python-to-TypeScript semantic differences that do not produce compilation errors but produce wrong results at runtime. The diff-based verification approach surfaced all of these systematically.

Bottom line: a migration I estimated at two weeks took five days. Claude handled the mechanical translation. I handled the semantic correctness.

Share this
← All Posts7 min read