pyforjs home

Syntax Rosetta & memory foundations

Map ES6+ onto Python primitives, then unlearn the parts that look identical but are not: nullity, truthiness, identity, and hashability.

You can read Python already. That is the problem. The syntax is close enough that you will translate it in your head and be wrong about four things per file. This module fixes the four.

Nullity

JavaScript ships two empty values. Python ships one.

JavaScriptPython
null, deliberately emptyNone
undefined, never assignedNone, or an AttributeError/KeyError
x ?? fallbackx if x is not None else fallback
obj?.keyd.get("key"), or attempt it and catch

None is a singleton, so is None is both the idiomatic and the correct test. == None works by accident until someone defines __eq__, and then it stops working.

nullity.py
Starting Python…

Truthiness

This is the divergence that silently changes behaviour when you port code.

truthy.ts
console.log(Boolean([])); // true — objects are truthy
console.log(Boolean({})); // true
console.log(Boolean("")); // false
console.log(Boolean(0)); // false
truthy.py
Starting Python…

In JavaScript, if (arr) asks does this array exist. In Python, if xs: asks does this list have anything in it. Ported directly, that check flips meaning. When you meant existence, say if xs is not None:.

Equality and identity

== compares values through __eq__. is compares object identity, it is Object.is, not ===. Reserve it for None, True and False.

identity.py
Starting Python…

Strings and slicing

Two traps live here.

JavaScriptPythonNote
`${name} is ${age}`f"{name} is {age}"interpolated at evaluation, not deferred
s.replace("-", "+")s.replace("-", "+", 1)Python replaces all by default
s.replaceAll("-", "+")s.replace("-", "+")
arr.at(-1)arr[-1]
arr.slice(1, 3)arr[1:3]
[...arr].reverse()arr[::-1]
slicing.py
Starting Python…

EAFP over LBYL

JavaScript style is Look Before You Leap. Guard with ?., then act. Python style is Easier to Ask Forgiveness than Permission. Act, then catch. A try block that raises nothing costs essentially nothing, so the guard buys you no performance and one more branch to keep in sync.

# LBYL, reads like JavaScript, and races if the dict is shared
if "key" in config and config["key"] is not None:
    use(config["key"])

# EAFP, idiomatic
try:
    use(config["key"])
except KeyError:
    use_default()

Hashability

Dict keys and set members must be hashable, which in practice means immutable. This has no JavaScript analogue, a Map accepts any object as a key, by reference.

hashable.py
Starting Python…

Comprehensions

.map().filter() chains become one comprehension. Same for dicts and sets. Swap the brackets for parentheses and you get a lazy generator instead of a materialised list, the equivalent of a function*, and what you want when the sequence is large.

pipeline.ts
const names = users.filter(u => u.age > 40).map(u => u.name);
const byId = Object.fromEntries(users.map(u => [u.id, u]));
const lazy = function* () { for (const u of users) yield u.name; };
pipeline.py
Starting Python…

Unpacking

JavaScriptPython
const [a, ...rest] = xsa, *rest = xs
const { x, y } = pointx, y = point["x"], point["y"]
{ ...obj, k: v }{**obj, "k": v} or obj | {"k": v}
[...a, ...b][*a, *b] or a + b

Kata

Two bugs, both of which look correct to a JavaScript reader.

kata_01.py
Starting Python…

Why the default argument breaks

def accumulate(item, into=[]) evaluates [] once, when the def statement runs, not on each call. Every invocation shares that one list. JavaScript evaluates default parameters per call, which is why the same shape is safe there. The Python idiom is into=None plus an if into is None: into = [] guard on the first line.

On this page