JavaScript and Python differences
Where JavaScript and Python behave differently in NEO scripts, and how to write a script that leaves the same result in both.
On this page
JavaScript and Python scripts run on the same engine and see the same World API, but each language keeps its own rules. [] ? 1 : 2 is 1 in JavaScript; 1 if [] else 2 is 2 in Python. NEO doesn't change either language. What it guarantees is that values crossing into the world are the same, so a script written carefully in both languages leaves the same state and effects.
This matters when you port a script from one language to the other, or when you keep twins of a script, as in Paired examples.
The mapping table#
| Topic | JavaScript | Python | To get the same result |
|---|---|---|---|
| Remainder | -7 % 3 is -1 (sign of the dividend) | -7 % 3 is 2 (sign of the divisor) | Write the rule out (see below). |
| Integer division | No operator: Math.floor(-7 / 2) is -4 | -7 // 2 is -4 | Math.floor(a / b) matches a // b. |
| Division | 7 / 2 is 3.5 | 7 / 2 is 3.5 | Same. |
| Rounding halves | Math.round(2.5) is 3, Math.round(-2.5) is -2 | round(2.5) is 2, round(-2.5) is -2 (halves to even) | Avoid rounding exact halves, or use Math.floor(x + 0.5) and math.floor(x + 0.5). |
| Number to text | String(1.0) is "1" | str(1.0) is "1.0" | Format numbers yourself if text must match. |
| Large integers | 2 ** 53 + 1 is 9007199254740992 (rounded) | 2 ** 53 + 1 is 9007199254740993 (exact) | Keep integers within ±2^53. |
| Divide by zero | 1 / 0 is Infinity | 1 / 0 raises ZeroDivisionError | Check the divisor first. |
| Truthiness | [] and {} are true; 0, "", NaN, null, undefined are false | Empty containers are false; 0, "", None are false; nan is true | Compare explicitly: list.length === 0 / len(xs) == 0. |
| String length | "😀".length is 2 (UTF-16 units) | len("😀") is 1 (code points) | Use [...s].length in JavaScript. |
| String index | "😀ab".indexOf("a") counts UTF-16 units | s.find("a") counts code points | Use [...s].indexOf(...) in JavaScript. |
| Equality | === compares objects by identity | == compares lists, dicts and tuples by value | Compare fields, not whole objects. |
| Map keys | Object keys are strings; integer-like keys come first when listed | 1, 1.0 and True are the same dict key | Use string keys that aren't numbers. |
| Default sort | [10, 9, 1].sort() gives [1, 10, 9] (sorts as text) | sorted([10, 9, 1]) gives [1, 9, 10] | Always pass a comparator in JavaScript: (a, b) => a - b. |
| Text operators | "3" * "4" is 12 | "3" * "4" raises TypeError | Don't rely on coercion. |
| JSON text | JSON.stringify({ b: 1, 2: 0, a: 2 }) is {"2":0,"b":1,"a":2} | json.dumps(...) keeps insertion order and adds spaces | Don't compare JSON strings across languages. |
Both languages agree on the things that come from the engine: the same random stream from this.rng.random(), the same maths functions, the same simulation clock, the same handler and timer order, and the same faults for running out of fuel or memory.
Writing integer maths portably#
JavaScript has no floor-division or floor-modulo operator. Define small helpers in your JavaScript script (they're plain code, not part of neo), and use Python's operators directly:
const floordiv = (a, b) => Math.floor(a / b);
const mod = (a, b) => ((a % b) + b) % b;
const pairs = [[7, 2], [-7, 2], [7, -2], [-7, -2], [0, 3]];
this.state.div = pairs.map(([a, b]) => floordiv(a, b)); // [3, -4, -4, 3, 0]
this.state.mod = pairs.map(([a, b]) => mod(a, b)); // [1, 1, -1, -1, 0]pairs = [(7, 2), (-7, 2), (7, -2), (-7, -2), (0, 3)]
self.state.div = [a // b for a, b in pairs] # [3, -4, -4, 3, 0]
self.state.mod = [a % b for a, b in pairs] # [1, 1, -1, -1, 0]Writing string code portably#
const s = "héllo 😀 wörld";
this.state.len = [...s].length; // 13
this.state.upper = s.toUpperCase();
this.state.rev = [...s].reverse().join("");
this.state.find = [...s].indexOf("w"); // 8s = "héllo 😀 wörld"
self.state.len = len(s) # 13
self.state.upper = s.upper()
self.state.rev = s[::-1]
self.state.find = s.find("w") # 8Numbers are canonicalised at the boundary#
Every value that reaches the world (state, emitted payloads, method arguments, fields) is converted to one shared set of types, the same from both languages:
- A whole number within ±2^53, except
-0, is an integer. It doesn't matter whether it came from a JavaScript number or a Pythonfloat:0.5 + 0.5is stored as the integer1in both. - Everything else is a float: fractions,
-0, infinities, NaN, and whole numbers beyond 2^53. Every NaN is stored the same way. - Python reads integers back as
int.self.state.x = 1.0reads back as1. JavaScript can't tell the difference. - Integers beyond 64 bits are refused in both languages (a JavaScript
BigIntor a Pythonintthat's too large raises an error). A stored integer beyond 2^53 reaches JavaScript as aBigInt, never as a rounded number. - Maps have string keys only. A Python dict with other keys can't cross.
- Strings are stored as text, so a string written by one language reads back identically in the other.
Names#
JavaScript uses camelCase and Python uses snake_case for the same state keys, fields and methods: this.state.lastOpener and self.state.last_opener are one key. The rule is in Script state.