I went into this port thinking the Rust part would be the hard part.
It was not.
The hard part was taking a small Python library that parses real-world price
strings and proving that the Rust version still behaves like the original,
including the awkward parts I would normally want to clean up.
This was my Code Resurrection 2026 / Port Mortem submission for Track D:
Python to Rust.
Project repo: https://github.com/AryanSaxenaa/PriceMortem
Source repo: https://github.com/scrapinghub/price-parser
What I ported
The source project, scrapinghub/price-parser, extracts price amounts and
currencies from raw text.
Simple example:
from price_parser import Price
Price.fromstring("$12.99")
That looks easy until you remember what price text looks like in the wild:
$12.99
12,99 €
USD 1,299.00
1.299,00 EUR
Free
Price unavailable
-$10
1e3 USD
A lot of parser behavior lives in those edge cases. Some of it is intentional.
Some of it is just what the original library already does.
For the hackathon, I did not want to ship a Rust parser that merely looked
reasonable. I wanted evidence that it matched the Python package.
The architecture I ended up with
The repo has three layers:
src/core.rs pure Rust parser logic
src/lib.rs PyO3 compatibility adapter
src/main.rs standalone Rust CLI
src/currencies_generated.rs source-derived currency table
The important split is between core.rs and lib.rs.
The parser core is Rust. It does not call into Python. The PyO3 layer exists so
the original Python tests can import price_parser.Price without being
rewritten.
That gave me two ways to run the port.
As a standalone Rust CLI:
cargo run -- '$12.99'
Output:
{"amount":"12.99","amount_text":"12.99","currency":"$"}
And as a Python-compatible package for the preserved upstream tests:
maturin build --features extension-module
python -m pytest
This was a useful compromise. The actual parser is in Rust, but the old test
suite can still exercise it through the public Python API.
The test suite was the oracle
The original pytest file is preserved in the port repo:
tests/original/test_price_parsing.py
Its recorded SHA-256 is:
dfb5d2ea7b7aff485ca89becd5a0090dac9f74f9e6b7c3c08a67cbb22e0bad32
That hash matters because editing the source tests would make the proof much
weaker. If a port only passes after the tests are adjusted, the port might be
right, but the evidence is no longer clean.
Current result against the Rust-backed package:
1061 passed, 134 xfailed
The 134 xfailed cases are not hidden failures from the port. They are
expected failures already marked by the upstream test suite. I kept them as-is
instead of rewriting them away.
I also kept native Rust tests for fast feedback:
7 passed, 0 failed
Those tests are useful, but they are not the main proof. The main proof is the
unchanged upstream test file passing against the Rust implementation.
Differential testing caught the things examples do not
Unit tests are good at checking known examples. Parsers need more than that.
I added differential testing that runs the same inputs against:
- the original Python package
- the Rust port through the compatibility adapter
Then it compares the structured outputs.
The latest 60-second fuzz run:
351 cases, 0 divergences
The fixed differential corpus includes cases across:
- currency symbols
- ISO currency codes
- Unicode spaces
- malformed prices
- locale-style separators
- negative values
- scientific notation
- empty strings
The useful part of this was not just the final zero-divergence number. It was
the process. Every mismatch forced a decision:
- Is the Rust implementation wrong?
- Is the source behavior surprising but intentional?
- Is this an upstream quirk that the port has to preserve?
For a behavioral port, "I can write this cleaner in Rust" is not automatically
a good reason to change behavior.
What broke
The most annoying issues were around ambiguity.
For example, separators are not just formatting. In one string, a comma may be
a decimal separator. In another, it may be a thousands separator. Sometimes the
currency appears before the amount, sometimes after it, and sometimes the input
contains text that only sort of looks like a price.
Currency matching also had some rough edges. The source has behavior around
currency symbols, ISO codes, and ordering that is easier to preserve once you
treat the source as the oracle instead of trying to redesign the rules from
memory.
The PyO3 boundary added another class of issues. The Rust core naturally wants
to produce Rust data. The original Python API returns Python-facing objects and
decimal.Decimal values. I kept Decimal conversion at the adapter boundary so
the Rust parser stayed independent while Python comparisons still matched what
the original tests expected.
The performance result was not flattering
The PyO3 compatibility path is slower than the source on hot-loop calls.
Current benchmark summary:
Rust/PyO3 adapter total time ratio: 9.76x slower than source
Rust/PyO3 adapter p99 ratio: 7.51x slower than source
Rust/PyO3 startup: 0.669s
Source startup: 1.346s
Rust/PyO3 RSS: 21.27 MB
Source RSS: 21.88 MB
That is not the number I would choose for a marketing page.
But it is the number I got from measuring the public Python-compatible path,
including extension calls and Decimal conversion. Hiding that would make the
submission weaker, not stronger.
The better next benchmark would separate the pure Rust CLI path from the PyO3
adapter path. For this submission, behavioral equivalence mattered more than
making the benchmark look good.
The decision I would take back
I should have added the standalone CLI earlier.
At first, the project leaned heavily on the PyO3 path because that was the best
way to run the original tests unchanged. That made sense for equivalence, but
it made the port look too Python-shaped from the outside.
Adding this helped:
cargo run -- '$12.99'
It shows the parser core can run as Rust without importing Python. The PyO3
adapter is still important, but now it is clearly a test and compatibility
layer, not the whole product.
What I would improve next
If I kept working on this after the hackathon, I would do five things:
- Increase Rust core coverage beyond the current line coverage.
- Add more fuzz inputs from real scraped ecommerce pages.
- Benchmark the pure Rust CLI separately from the Python adapter.
- Publish a small Rust crate API around the core parser.
- Keep the Python adapter optional for compatibility testing.
Final result
The final port has:
- preserved upstream tests with a recorded SHA-256
- unchanged pytest parity through a PyO3 adapter
- a standalone Rust CLI
- native Rust tests
- differential fuzzing
- benchmark disclosure
- coverage reporting
- zero unsafe Rust blocks in the project code
The main lesson was simple: generating a port is not the finish line.
For this kind of rewrite, the real work is building enough evidence that the
new implementation deserves to be trusted.



Top comments (0)