Porting code to another language with AI: migration plan and checks

AI translates code between languages quickly, but it will not preserve behaviour on its own. A module migration plan with reference tests and comparison.

5 min · · development · автоматизация

You can rewrite code in another language with AI in hours instead of weeks, but only if you decided in advance how you will prove that the new code does the same thing. A model translates syntax well, while implicit behaviour (rounding, iteration order, handling of empty values, encodings) gets lost easily. Below is a migration plan for one module: inventory, reference tests, piece-by-piece porting, behaviour comparison and a cost estimate. As an example we take an invoice-parsing utility in Python that we port to Go.

Step 1. Inventory

Before the first request to the model, list what you are porting. You can build the list together with the model, but you verify it:

  • the public interface: functions called from outside, their arguments and return values;
  • dependencies: third-party libraries and what replaces them in the target language;
  • side effects: files, network, database, environment variables, time;
  • places with "odd" behaviour: regular expressions, dates and money, sorting, rounding, unicode.

The last item is the most important. That is where languages diverge: integer division, handling of None and nil, dictionary key order, overflow behaviour. Ask the model to find such places and list them as risks, then go through the list yourself.

Step 2. Reference tests before the port

The migration rule: tests are written on the old code and before the port. If there are no tests, create "golden" data: run the old implementation on representative inputs and save the results.

bash
import json  # make_golden.py: run on the OLD code
from pathlib import Path
from billing.parser import parse_invoice  # old implementation

cases = []
for path in sorted(Path("samples").glob("*.txt")):
    text = path.read_text(encoding="utf-8")
    cases.append({"name": path.name, "input": text, "output": parse_invoice(text)})

Path("golden.json").write_text(
    json.dumps(cases, ensure_ascii=False, indent=2, sort_keys=True),
    encoding="utf-8",
)

The samples should include edge cases: an empty file, a very long line, Cyrillic, a wrong format, zero and negative amounts. The model can help invent such inputs, but take the result of the run only from the real old implementation.

Step 3. Port in pieces

Do not ask "rewrite the whole project". Move bottom-up through the dependency graph: pure functions without side effects first, then modules with input and output, the entry point last. For each piece:

  1. Give the model the function's source, its tests and the list of risks for it.
  2. Ask for idiomatic code in the target language, not a line-by-line translation: an indexed loop is written differently in Python and Go.
  3. Ask it to list separately the places where behaviour may differ.
  4. Run the reference tests. Analyse failures with the model, showing it the real output.

An agent that runs the tests itself is convenient for all this: Claude Code or another tool of that kind. Then the "edit, run, error, edit" loop does not need your involvement, and you read the final diff.

Step 4. Behaviour comparison

Reference files are not enough. Add a comparison of the two implementations on the same inputs, including random ones:

bash
import json  # compare.py: runs old and new implementations on the same data
import subprocess
from pathlib import Path

def run_old(text: str) -> object:
    from billing.parser import parse_invoice
    return parse_invoice(text)

def run_new(text: str) -> object:
    p = subprocess.run(
        ["./invoice-go"], input=text, capture_output=True,
        text=True, encoding="utf-8", timeout=10,
    )
    return json.loads(p.stdout)

bad = 0
for path in sorted(Path("samples").glob("*.txt")):
    text = path.read_text(encoding="utf-8")
    if run_old(text) != run_new(text):
        bad += 1
        print("MISMATCH:", path.name)
print("mismatches:", bad)

Compare parsed structures, not strings, and decide separately what counts as equal: key order, floating-point formatting, whitespace. Every mismatch is either a porting error or a bug in the old code that you have now found. The second happens too, and a human decides whether to repeat the bug or fix it.

Step 5. What not to trust without checking

  • Multithreading and concurrency. A model easily reproduces data races, especially when porting synchronous code to asynchronous.
  • Money and time. Check types: float instead of decimals, default time zones.
  • Error handling. An exception in Python, a returned value in Go, a Result in Rust; make sure an error is not lost silently.
  • Dependencies. A model can "find" a library that does not exist. Verify each dependency you add in the package registry.
  • Performance. A correct port does not guarantee a fast one; measure.

What it costs

The estimate is based on code size. Assumptions (illustrative, check on your project): a 12,000-line module, about 10 tokens per line, so roughly 0.12M tokens. The exact number comes from token counting: it runs no generation, but the request itself is paid, with tokens billed at the model's input price.

One pass (read the source and write the translation) without cache: input 0.12M × input price + output 0.12M × output price. Zukko prices as of October 18, 2026:

  • claude-sonnet-5-5: 0.12 × 0.4 + 0.12 × 2 = 0.048 + 0.24 = $0.288.
  • claude-opus-5-5: 0.12 × 0.8 + 0.12 × 4 = 0.096 + 0.48 = $0.576.
  • gpt-6.1-sol: 0.12 × 0.27 + 0.12 × 1.32 = 0.0324 + 0.1584 = $0.191.

The real sum is larger: add tests, fixes and reruns. If you assume three or four iterations per piece, multiply by three or four. Agent tools add the accumulated context on every step, so it helps to estimate the budget in advance and set a limit on the key. For porting a large project, start with one module, look at the actual spend in the "Usage" section of the dashboard and only then scale. See also: how to reduce token spend.

We cannot say in advance which model will handle your stack better. Strength index in our catalog: Opus 5.5 is 58, GPT-6.1 Sol is 52, Sonnet 5.5 is 48. Take one function of medium complexity, run it through two or three models and compare by the reference tests.

Connecting

Any SDK works for scripts: create a key in the dashboard, use the base URL https://api.zukko.pro/v1 for an OpenAI-compatible client, and the address without /v1 for Claude Code and the Anthropic SDK. Access from Russia goes through the gateway; payment by SBP, MasterCard card or cryptocurrency.

FAQ

Can I rewrite a whole project in one request?

Technically a context of up to 1M tokens in most catalog models fits a lot of code, but that does not improve verification. It is more reliable to port in pieces and check each with reference tests.

Will an AI code generation tool check by itself that the code works?

The model itself will not: it does not run code. Tests and agent tools that execute commands do that. That is why you need tests before the port.

What if the project has no tests?

Create golden data from the old implementation (step 2). It does not replace full tests, but it fixes the current behaviour, and preserving it is exactly the goal of the migration.

Which language is easiest to port?

It depends on the pair. Languages close in paradigm port more easily. But real difficulty is more often in libraries and implicit quirks than in syntax, so judge by the inventory.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Rewrite code in another language with AI: a migration plan · Zukko