JavaScript vs Python: Finding Hidden Rounding Errors in Financial Code | Mycroft

Tanmay Kulkarni ports a financial contradiction detector from JavaScript to Python and checks that both versions agree. A three-match process, plus edge-case test companies, exposes rounding, threshold and reporting differences before the code goes live.

4:31 video4 min readWatch on YouTube

Round 0.625 to two decimal places in JavaScript and you get 0.63. Do the same in Python the everyday way and you get 0.62. Same number, same request, two answers. In this walkthrough, Tanmay Kulkarni explains how he checked that a Python rewrite of a finance detector still said the same thing as the JavaScript original, using a method built around three matches.

The setup: a recipe and a contradiction detector

The work is a contribution to Mycroft, which describes itself as both a book and a working agentic repository for finance. Its recipes are reusable pipelines. He picked one, a contradiction detector, and rewrote its core logic from the original workflow's JavaScript into Python. The detector reads five kinds of evidence about a company: the guidance it gives on earnings calls, the risks it admits, the questions analysts pressed on, news sentiment and engineering activity. Six patterns look for places where two of those disagree, and every disagreement becomes a flag for a person to review. To test it, he invented 16 companies whose names all end in Fixture Co, so no real company is judged.

Match one: write the expected answers first

The first match is his own reading against the original. Before writing any Python, he went through the JavaScript pattern by pattern and wrote down the flags it should raise for each of the 16 companies, 13 in total. He did this first because if the expected answers are written after the code exists, they tend to copy whatever the code says, mistakes included.

Match two: run both versions side by side

The second match is the real comparison. A parity check takes the original JavaScript straight out of the workflow file and runs it beside the Python on the same 16 companies, comparing every flag and every sentence of evidence word for word. All 16 agreed. The video then asks the sharper question: a match only means something if a mismatch could have shown up. The known places where the languages differ include the rounding tie, the way a value of 1.0 prints (as 1 in JavaScript and 1.0 in Python), and how a fallback written with two vertical bars treats an empty list. He made the Python copy JavaScript's behavior on each point on purpose.

Proving the check can fail

To test the comparison itself, he made small changes to his own copy: moving one threshold from 0.6 to 0.61, and swapping in Python's own rounding. The comparison still said everything agreed. The test companies simply never stood on the line. None sat at exactly 0.6, and no news average landed on a tie. So he added two companies built to stand exactly there. Romeo Fixture Co gives guidance at a confidence of exactly 0.6, and Quebec Fixture Co has a news average of exactly 0.625. With the same two changes, each one now fails on the company placed for it. The fix was not new code but better test data.

Match three: the report against the log

The third match was closer to home, since both sides were his. At the last review he compared the human report with the machine log for a run designed to stop early on bad data. The report said 13 rows were rejected, while the log said zero, because the log was only counting a later step that never ran. Mycroft's own rules say no artifact silently wins. Now one list feeds both files, anything not checked says not checked, and a new test compares the two.

Limits and the add-never-remove rule

Everything ran on invented data, with no database, news feed or AI model called. Live data, the model's answers and a real service timing out were not tested. At the approval step he declined live mode on the record and wrote down what would need to be true first, since that call belongs to people with live access. In a shared repository he held to one rule: add, never remove. Every change was a new file for this recipe or an update to its own page, plus a few lines added to the end of two shared files.

Key takeaways

  • The same value can round differently in JavaScript and Python, so a port needs deliberate checking.
  • Write expected results before the new code exists, so the code cannot shape the answers.
  • Run the original and the port side by side and compare output field by field.
  • A passing comparison only counts if a mismatch could have appeared.
  • Place test cases exactly on thresholds and rounding ties to expose hidden differences.
  • Compare human-readable reports with machine logs, since they can tell different stories.
  • State plainly what was not tested, such as live data and model answers.

Try it yourself

Think of something you have rewritten in a new language, tool or cleaner version. Ask where the two versions would most likely disagree, find that one spot, and make sure a single test case is standing exactly on it.

Chapters

  1. 0:00Introduction: The 0.625 Rounding Trap in JS vs Python
  2. 0:40Mycroft Recipe Architecture & The Contradiction Detector - Match 1: Writing Expected Answers Before Writing Python Code - Match 2: Running JS & Python Side-by-Side (16 Test Companies) - Forcing Mismatches: Rounding & Threshold Differences - Testing Edge Cases: Romeo Fixture Co & Quebec Fixture Co - Match 3: Human Report vs Machine Log Discrepancies
Full transcript(auto-generated, with timestamps)

Introduction: The 0.625 Rounding Trap in JS vs Python

[0:00]Here's one number, 0.625. Ask JavaScript to round it to two decimal places and you get 0.63. Ask Python the same thing the everyday way and you get 0.62. Same number, same request, two answers. I had just rewritten a finance detector from one of those languages into the other. So the question I couldn't put down was this. If my version had quietly done that somewhere, would I have noticed? Hi, this is Tanme Kulcarnney in for humanitarians AI. This week I worked on a contribution to my craft which describes itself as both a book and a working agentic repository for finance. Its recipes are reusable pipelines. I picked one a contradiction detector and rewrote its core logic from the original workflows JavaScript into Python. So the

Mycroft Recipe Architecture & The Contradiction Detector - Match 1: Writing Expected Answers Before Writing Python Code - Match 2: Running JS & Python Side-by-Side (16 Test Companies) - Forcing Mismatches: Rounding & Threshold Differences - Testing Edge Cases: Romeo Fixture Co & Quebec Fixture Co - Match 3: Human Report vs Machine Log Discrepancies

[0:40]Recipe is scripts that run. This video is about one thing. How I checked that my version still says the same thing. It came down to three matches. Here's what it does. It reads five kinds of evidence about a company. The guidance it gives on earnings calls. the risks it admits, the questions analysts pressed on, news sentiment and engineering activity. Six patterns look for places where two of those disagree, and every disagreement becomes a flag for a person to review. To test it, I invented 16 companies. Every name ends in fixture code. No real company is judged in this video. The first match is my own reading against the original. Before writing a single line of Python, I did something slower. I went through the original JavaScript pattern by pattern and wrote down the flags it should raise for each of those 16 companies. 13 flags

Written down first because if I write the expected answers after my code exists, I'll just write down whatever my code says, mistakes included. Second match is the real comparison. My par check takes the original JavaScript straight out of the workflow file. It runs it right beside my Python on the same 16 companies and compares the two field by field. Every flag, every sentence of evidence, word for word. 16 out of 16 agreed and that felt good for about a minute. Then it started to bother me. A match only tells you something if a mismatch could have shown up. So where could these two actually differ? Places I knew about JavaScript and Python don't always agree. That rounding tie is one. Another JavaScript prints 1.0 is just one while Python prints 1.0. And a fallback written with two vertical bars keeps an empty list in JavaScript

Where Pythons or skips past it. A flag's wording carries numbers like these. So, I made my Python copy JavaScript's behavior on each one on purpose. Here's the part I want to be honest about. To see whether my comparison would notice a difference, I made small changes to my own copy. I moved one threshold from 0.6 to 0.61. I swapped in Python's own rounding, and the comparison said everything still agreed. It was doing its job. My test companies just never stood on the line. Not one of them sat at exactly 0.6 and no news average landed exactly on a tie. I added two companies built to stand exactly there. Romeo Fixture Co. gives guidance at a confidence of exactly 0.6. Quebec Fixture Co. has a news average of exactly 0.625. Run the same two changes again and each one fails on the company placed for it.

The threshold on Romeo, the rounding on Quebec, the fix wasn't new code. It was better test data. The third match was closer to home because both sides were mine. At the last review, I put the human report side by side with its machine log for the run that's designed to stop early on bad data. The report said 13 rows were rejected. The log said zero. Same run, two different stories, and I wrote both of them. The log was only counting a later step, one that never got to run. Microoft's own rules say no artifact silently wins. So now, one list feeds both files. Anything that wasn't checked says not checked, and a new test compares the two. Everything you've seen ran on invented data. No database, no news feed, and no AI model was ever called. Live data, the model's answers. What happens when a

Real service times out? None of that is tested. So, at the approval step, I declined live mode on the record and wrote down what would need to be true first. That call belongs to the people with live access. Because this is a shared repository, I held myself to one rule. Add never remove. Every change is a new file for this one recipe or an update to this own page, plus a few lines added to the end of two shared files. Nothing deleted, nothing renamed. So, if you've ever rewritten something you depend on in a new language, a new tool, or just a cleaner version, here's the question I'd leave you with. Where would your two versions most likely disagree? Find that one spot and make sure a single test is standing exactly on it. Same logic, two languages. Tanme Kulcarnney in for humanitarians AI signing off.

More from Mycroft Financial AI

Humanitarians AI Lyrical Literacy Project