Loading your place in this lesson…

Your JavaScript, sorted into three bins

After this you can read the lab scripts of chapters 1 and 3 and predict what they print.

Three bins, one file

Guess first Python's for x in xs: hands you each item of the list xs. Which JavaScript loop hands you the same thing?

In chapter 1 you ran speed.mjs or its twin speed.py. Same job, two languages. Read the top of each, side by side.

Python has no braces. A colon opens a block, and the lines indented under it are the block. Use 4 spaces per level.

Every pair in this chapter carries one of three tags. Tag 1, Same in Python: same job, other spelling. JSON.parse is json.loads.

The keywords read like English: for x in xs: is for...of, not for...in; try/except/raise are try/catch/throw; and, or, not are &&, ||, !.

process.argv[2] is sys.argv[1]: the same list, one place off. Node's list starts with node itself, then your script; Python's starts with your script.

Tag 2, Looks the same, acts different: a look-alike that acts differently. msg.message?.content became msg.get("message", {}).get("content").

Tag 3, New in Python: no twin in JavaScript. Examples: with, **kwargs and [:, -1, :]. Later steps teach all three.

Quick check process.argv[2] in Node is which item in Python?

Quick check In a Python file, what ends an if block?

Values: true, false and None

Guess first Predict both: bool([]) in Python and Boolean([]) in JavaScript.

Guess first In Python, t = 0. What is t or 30?

Truthiness differs: Python treats '', [], {}, 0 and None as false. JavaScript treats [] and {} as true. So if not rows: means empty.

None is null, and JSON null loads as None. There is no undefined. Test with is None, as speed.py line 33 does.

Common mistake: or is not ??. 0 or 30 gives 30: a valid 0 is lost. The fix: 30 if t is None else t.

That fix is Python's test ? a : b, written value first: a if test else b. speed.py line 11 uses one.

Where JavaScript gives undefined, Python raises IndexError for a list, KeyError for a dict.

d.get(key, default) is your ?.: a missing key gives the default, never an error. speed.py line 31 chains two of them.

One catch: JSON null arrives as None, a value, so the default is skipped and the next .get fails. (d.get("message") or {}) is safe.

A dict has keys, not properties. msg.done raises AttributeError. Parsed JSON is a dict, so write msg["done"] or msg.get("done").

== never turns text or a list into a number: 0 == '' and [] == False are False. Only 0 < '' raises TypeError.

is tests identity, the same object; it is not ===. Use == for values, and is only for None: t is not None keeps 0.0.

Quick check In Python, user_timeout = 0, then timeout = user_timeout or 30. What is timeout?

Quick check msg = {"done": True}. What does msg["message"]["content"] do?

Quick check An elapsed time s is exactly 0.0. Which line prints none for it? (f"{s:.2f} s" works like a template literal: s with two decimals.)

Numbers, lists and dicts

Guess first Predict both: round(2.5) in Python and Math.round(2.5) in JavaScript.

Guess first In JavaScript, 1 / 0 gives Infinity. What does Python do with 1 / 0.0?

Python keeps whole numbers (int) apart from decimals (float). / always gives a float, so 6 / 2 is 3.0; 6 // 2 is 3.

// is Math.floor(a / b), not Math.trunc: -7 // 2 is -4, where Math.trunc(-7 / 2) gives -3.

Divide by zero and Python stops with ZeroDivisionError, even for 1 / 0.0. JavaScript gives Infinity and carries on.

Real case: chapter 1's speed.py divides by the time from first to last answer chunk. One chunk makes that 0.0, and line 50 crashed.

Python rounds a halfway value to the even side: round(2.5) is 2, and 22.25 to one decimal is 22.2. JavaScript gives 3 and 22.3.

So the same tokens/s can print a different last digit in speed.py and speed.mjs. Compare numbers, not text: math.isclose(a, b, rel_tol=0.05) allows 5%.

xs.sort() sorts in place and returns None; JavaScript's sort() returns the array. For a new sorted list, write sorted(xs).

map() gives a one-use sequence, like a generator: len() fails, and a second pass is empty. JavaScript's map() gives an array.

Write a comprehension instead: [f(x) for x in xs] builds a list, like xs.map(f). Chapter 1's practice_table.py builds its lists this way.

A dict is not an object: keys keep their type. {1: 'a', '1': 'b'} has two keys; JavaScript turns 1 into '1'.

xs[-1] is the last item, like JavaScript's xs.at(-1). In JavaScript, xs[-1] is undefined.

A slice xs[2:10] copies, like slice(2, 10), and stops quietly at the end of the list. A single index past the end, xs[10], raises IndexError.

In Python, text plus a number, 'tokens: ' + 5, raises TypeError. JavaScript turns the 5 into text. Write str(5) or an f-string (part 4).

Quick check What is -7 // 2 in Python?

Quick check xs = [3, 1, 2], then ys = xs.sort(), then print(ys). What prints?

Quick check Printed to one decimal, a tokens/s of exactly 22.25 shows 22.2 in Python and 22.3 in JavaScript. What should your port's test compare?

Functions and classes

Guess first def record(ttft, runs=[]): appends ttft to runs and returns it. You call record(0.4), then record(0.5). What does the second call return?

Guess first fns = [lambda: i for i in range(3)] makes three small functions, one for each i in 0, 1, 2 (lambda: i is () => i). What does [f() for f in fns] give?

def (Python's function) makes a default once: runs=[] is one list, shared by every call.

That is a measurement bug: record keeps every earlier experiment's samples, so the median is wrong. Fix: runs=None, then make the list inside.

Python can pass arguments by name: urlopen(req, timeout=600). These are keyword arguments. JavaScript passes an options object instead, as fetch does.

**inputs passes each key of a dict as a keyword argument. Without the **, the whole dict lands in the first parameter, with no error.

Classes read like JavaScript's. __init__ is the constructor, and self is this, written out as the first parameter of every method.

Run both: the same line. report takes self, yet you pass only the seconds: Python passes self for you.

A decorator, @logged above def draft():, means draft = logged(draft): one function wrapped by another, as the JavaScript twin does by hand.

Run both: the wrapper prints first, then the draft. Labs meet decorators such as @unittest.skipIf and @torch.no_grad().

with is a built-in try/finally: its context manager cleans up, even after an error. JavaScript's newer using does this; Node 22 lacks it.

f-strings are template literals: f'TTFT {ms:.1f} ms', where :.1f means one decimal. Forget the f and the braces print as text.

Type hints are like TypeScript types that Node strips before it runs: nothing checks them at run time. mypy or pyright does tsc's job.

A Python loop reuses one variable, so closures made in a loop all see its last value. Pass the value in as an argument.

Assigning a name inside a function makes it local. To change a top-of-file name, write global T0 first, as five_at_once.py line 37 does.

Quick check inputs is a dict. What does the ** do in model.generate(**inputs)?

Quick check Which record() is safe to call for two experiments in a row?

Quick check @logged sits on the line above def draft(order):. Which line does the same job?

Reading a lab script

Guess first A Python script stops with a traceback a dozen lines long. Where is the name of the error?

Lab scripts read their flags with argparse: --limit 5 becomes args.limit. Node's twin is util.parseArgs.

Run it with --help: argparse prints every flag it knows, with the help text the script gave it, then stops.

A loop can unpack pairs (tuples), like destructuring: for i, x in enumerate(xs) is xs.entries(), and for k, v in d.items() is Object.entries(d).

Run both with --limit 2: each prints the same eight lines. Here --limit is only read and printed.

import timing (chapter 1's timing.py) searches by name, and the script's own folder comes first. So a statistics.py there hides the real statistics module.

The main guard, if __name__ == "__main__":, runs its block only when the file is the program, not when imported. Node's twin: import.meta.main.

Read a Python traceback bottom-up: the last line names the error, and the lines above it show where. Node prints the error near the top.

Run both: each stops with an error on purpose. Node's flag keeps its list of calls to one.

Pass encoding="utf-8" to every open(). Without it, Python 3.13 uses the machine's own text encoding, which differs between machines.

Quick check In your lab folder, statistics.median(...) raises AttributeError: module 'statistics' has no attribute 'median'. What is wrong?

Quick check A traceback's first File line names five_async.py, line 75. Its last File line names speed_port.py, line 62. Its last line is KeyError: 'message'. Where did the error happen?

Quick check A lab script reads its flags with argparse. What is the quickest way to see every flag it takes?

Go deeper