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.
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.
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.
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
| JavaScript | Python |
|---|---|
function f(...args) | def f(*args), packs into a tuple |
| untyped options bag | def 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.
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.
| JavaScript | Python | Enables |
|---|---|---|
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() | @property | x.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).
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.
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.
Syntax Rosetta & memory foundations
Map ES6+ onto Python primitives, then unlearn the parts that look identical but are not: nullity, truthiness, identity, and hashability.
Asynchronous runtimes & concurrency
Node's event loop is always running; Python's is not. Lazy coroutines, explicit scheduling, and the GIL's effect on which concurrency primitive you actually want.