pyforjs home

Scoping, closures & dunder protocols

Replace the prototype chain with Python's explicit object model. LEGB lookup, nonlocal, and the dunder methods that make your classes feel built-in.

There is no prototype chain. There is no this binding to reason about. In exchange you get an explicit lookup order and a set of named protocol hooks. That is more verbose, and considerably easier to predict.

LEGB

Name lookup walks four scopes in order: Local → Enclosing → Global → Built-in. One rule catches people. Assigning to a name anywhere in a function makes it local for the entire function, including the lines above the assignment.

legb.py
Starting Python…

JavaScript has no equivalent. let n = 0 in an outer scope is simply writable from a closure. Python makes you declare the intent. Use nonlocal for an enclosing function scope, global for module scope.

counter.ts
function makeCounter() {
let n = 0;
return () => {
n += 1;
return n;
};
}
counter.py
Starting Python…

Late binding

Closures capture the variable, not its value at creation time. This is the pre-ES6 var bug, and Python has no let to fix it with.

late_binding.py
Starting Python…

The fix deliberately uses the mutable-default mechanism from Module 1: a default argument is evaluated when the lambda is created, which is exactly the snapshot you want.

Signatures

JavaScriptPython
function f(...args)def f(*args), packs into a tuple
untyped options bagdef f(**kwargs), packs into a dict
f({ apiKey, timeout = 30 })def f(*, api_key, timeout=30)
f.apply(null, xs)f(*xs)

Everything after a bare * is keyword-only. It is the cheapest API design tool Python has. It stops callers passing three positional booleans, and makes every call site self-documenting.

signatures.py
Starting Python…

Dunder protocols

Where JavaScript has Symbol.iterator, toString and a handful of well-known symbols, Python has a full protocol table. Implement the method, get the syntax.

JavaScriptPythonEnables
obj.toString()__repr__ / __str__repr(x), str(x), printing
arr.length__len__len(x)
obj[key]__getitem__x[0], x[1:3], x["k"]
for (const v of obj)__iter__ / __next__for v in x
callable object__call__x()
operator overload, none__add__, __eq__, __lt__a + b, a == b, sorted()
get x()@propertyx.total without parentheses

__init__ is the constructor. self is explicit, it is the first parameter of every method, and nothing binds it for you. That verbosity is why Python never needed .bind(this).

protocols.py
Starting Python…

MRO

Python supports multiple inheritance and resolves it with C3 linearization. super() follows the MRO, not "the parent class". In a diamond, super() inside A may well call B.

mro.py
Starting Python…

Read it left to right

C(A, B) linearizes to C, A, B, Base, object. A.who calls super().who(), which resolves to the next class in C's MRO, B, not Base. This is why cooperative multiple inheritance requires every class in the chain to call super().

Properties

@property turns a method into a read-only attribute. Unlike a JavaScript getter it can be retrofitted onto an existing public attribute without changing a single call site, which is why Python code rarely has getX() methods in the first place.

Kata

Build a collection that behaves like a built-in.

kata_02.py
Starting Python…

On this page