Python is not merely JavaScript without braces. Indentation defines blocks, names refer to objects, collections have different guarantees, and direct iterable traversal replaces many index-based loops. This chapter aims to make an unfamiliar file readable before you can write a library yourself.
Understanding: Python syntax for a JavaScript developer
You can read this chapter before running the experiment. Then return to the live trace and match each concept to a real event.
A .py file consists of statements and expressions. Assignment binds a name to an object rather than declaring a fixed-type storage cell. A colon starts a suite whose extent is defined by indentation. def creates a function object, import executes and caches a module, and for requests items from an iterable.
Terms used in this experiment
Understand the words first, then the execution order.
Name
An identifier bound to an object. Reassignment changes the binding rather than the type of a dedicated variable cell.
Statement
An instruction such as if, for, import, return, or assignment that controls execution.
Expression
A fragment evaluated into an object value, such as a call, arithmetic expression, comprehension, or subscription.
Iterable
An object whose items can be requested one by one by for, a comprehension, sum, list, and other consumers.
list
A mutable ordered sequence. It resembles a JS Array, but its methods and copy model differ.
tuple
An immutable sequence commonly used for a fixed group of values and unpacking.
dict
A key-to-value mapping. Keys must be hashable; [] raises KeyError for a missing key while get returns a fallback.
set
A collection of unique hashable items with fast membership tests and set operations.
None
The singleton object representing no value, normally compared with is None.
Comprehension
An expression that builds a list, dictionary, or set from an iterable with transformation and optional filtering.
What happens step by step
Each step maps to an observable runtime state.
- 01The interpreter loads a module
Top-level statements execute in order, so import is not textual inclusion and may have side effects.
- 02Indentation forms a block
A colon opens a suite after if, for, def, class, try, with, or match; equal indentation shows membership.
- 03Assignment binds a name
orders = [...] creates a list and binds orders to it. Another name may reference the same list.
- 04for requests items
for order in orders receives objects from the iterable. Use enumerate only when an index is genuinely needed.
- 05A comprehension builds a collection
The left expression transforms, for chooses the source, and a trailing if filters.
- 06def creates a function
The body does not run at definition time. Parameters bind to passed objects on a call.
- 07return ends the call
Without an explicit return a function returns None; several comma-separated values form a tuple.
Where the result needs context
These details explain why similar code can sometimes produce a different trace.
and and or return an operand
The result need not be bool, so value or fallback incorrectly replaces valid zero, empty text, or an empty list.
== and is ask different questions
== checks value equality while is checks object identity. Use is for None and private sentinel objects.
A slice is normally a shallow copy
items[:] creates a new list that still contains references to the same nested objects.
dict is not a JavaScript object
Keys may have multiple hashable types, missing [] raises KeyError, and attribute access is a different protocol.
Annotations do not validate runtime values
name: str helps an IDE and type checker, but ordinary CPython does not enforce the binding type.
Loops may have else
The else branch runs when the loop finishes without break; it does not belong to the nearest if.
Scope is not block-based
if and for do not create local scopes. Modules, functions, classes, and comprehensions normally do.
First understand which parts of Node participate in execution.
Then remove instrumentation and focus on the central mechanism.
Finally match the model to the code that produces the live trace.
A minimal model without instrumentation
orders = [
{"id": "A-10", "status": "paid", "price": 120},
{"id": "A-11", "status": "draft", "price": 80},
]
paid_ids = [
order["id"]
for order in orders
if order["status"] == "paid"
]
def describe(order, *, currency="RUB"):
return f"{order['id']} · {order['price']} {currency}"The complete code executed by the scenario
This is not an alternative example: these are the functions and files used by the Run button.
The first file is the real CPython scenario; the second safely starts the child process and turns its JSON Lines into the live trace.
#!/usr/bin/env python3
"""Fixed, input-free CPython scenarios used by Runtime Lab."""
from __future__ import annotations
import asyncio
import dis
import gc
import io
import json
import platform
import sys
import time
import weakref
from dataclasses import dataclass
from typing import Any, Iterator
def emit(lane: str, event_type: str, key: str, **data: Any) -> None:
print(
json.dumps(
{"lane": lane, "type": event_type, "key": key, "data": data},
ensure_ascii=False,
),
flush=True,
)
def version_event() -> None:
emit(
"python",
"runtime",
"python.version",
implementation=platform.python_implementation(),
version=platform.python_version(),
)
def run_syntax() -> None:
version_event()
orders = [
{"id": "A-10", "status": "paid", "price": 120, "qty": 2},
{"id": "A-11", "status": "draft", "price": 80, "qty": 1},
{"id": "A-12", "status": "paid", "price": 50, "qty": 3},
]
emit("objects", "state", "syntax.objects", count=len(orders))
paid = [order for order in orders if order["status"] == "paid"]
total = sum(order["price"] * order["qty"] for order in paid)
emit(
"comprehension",
"result",
"syntax.comprehension",
ids=", ".join(order["id"] for order in paid),
total=total,
)
first, *middle, last = [order["id"] for order in orders]
emit(
"sequence",
"result",
"syntax.unpack",
first=first,
middle=middle,
last=last,
)
labels = [f"{index}:{order['id']}" for index, order in enumerate(orders, start=1)]
emit("loop", "result", "syntax.enumerate", labels=", ".join(labels))
def format_order(order: dict[str, Any], *, currency: str = "RUB") -> str:
return f"{order['id']} · {order['price'] * order['qty']} {currency}"
emit(
"function",
"result",
"syntax.function",
rendered=format_order(orders[0], currency="₽"),
)
emit("result", "result", "syntax.result")
@dataclass(slots=True)
class CartLine:
sku: str
price: int
quantity: int = 1
@property
def subtotal(self) -> int:
return self.price * self.quantity
def append_bad(item: str, bucket: list[str] = []) -> list[str]:
bucket.append(item)
return list(bucket)
def append_safe(item: str, bucket: list[str] | None = None) -> list[str]:
target = [] if bucket is None else bucket
target.append(item)
return target
def even_squares(limit: int) -> Iterator[int]:
for value in range(limit):
if value % 2 == 0:
yield value * value
def classify_event(event: dict[str, Any]) -> str:
match event:
case {"type": "order.paid", "payload": {"id": order_id}}:
return f"paid order {order_id}"
case {"type": event_type}:
return f"other event {event_type}"
case _:
return "invalid event"
def run_semantics() -> None:
version_event()
line = CartLine("book", 450, quantity=2)
emit(
"class",
"result",
"semantics.dataclass",
rendered=repr(line),
subtotal=line.subtotal,
)
original = ["node"]
alias = original
alias.append("python")
emit("objects", "mutation", "semantics.alias", shared=original == alias == ["node", "python"])
bad_first = append_bad("api")
bad_second = append_bad("worker")
emit(
"function",
"warning",
"semantics.mutable-default",
first=bad_first,
second=bad_second,
)
safe_first = append_safe("api")
safe_second = append_safe("worker")
emit(
"function",
"result",
"semantics.safe-default",
first=safe_first,
second=safe_second,
)
emit("generator", "result", "semantics.generator", values=list(even_squares(7)))
emit(
"pattern",
"result",
"semantics.match",
label=classify_event({"type": "order.paid", "payload": {"id": "A-42"}}),
)
try:
int("not-a-number")
except ValueError as error:
emit("exception", "caught", "semantics.exception", message=str(error))
stream = io.StringIO()
with stream:
stream.write("cleanup is deterministic")
text = stream.getvalue()
emit("context", "cleanup", "semantics.context", closed=stream.closed, text=text)
emit("result", "result", "semantics.result")
def doubled_total(values: list[int]) -> int:
return sum(value * 2 for value in values)
class CycleNode:
def __init__(self) -> None:
self.peer: CycleNode | None = None
async def traced_task(name: str, delay: float, completion: list[str]) -> str:
emit("asyncio", "start", "asyncio.started", name=name)
await asyncio.sleep(delay)
completion.append(name)
emit("asyncio", "resume", "asyncio.resumed", name=name)
return name
async def measure_timer_while(blocking_call: Any) -> float:
started = time.perf_counter()
timer = asyncio.create_task(asyncio.sleep(0.01))
await blocking_call()
await timer
return max(0.0, (time.perf_counter() - started - 0.01) * 1_000)
async def run_asyncio_round() -> None:
completion: list[str] = []
first = asyncio.create_task(traced_task("A", 0.025, completion))
second = asyncio.create_task(traced_task("B", 0.005, completion))
emit("asyncio", "schedule", "asyncio.created")
await asyncio.gather(first, second)
emit("asyncio", "result", "asyncio.result", order=" → ".join(completion))
async def block_loop() -> None:
time.sleep(0.055)
async def offload_sleep() -> None:
await asyncio.to_thread(time.sleep, 0.055)
blocked_delay = await measure_timer_while(block_loop)
emit("asyncio", "blocking", "asyncio.blocking", delay=round(blocked_delay, 1))
offloaded_delay = await measure_timer_while(offload_sleep)
emit("asyncio", "offload", "asyncio.offload", delay=round(offloaded_delay, 1))
def run_runtime() -> None:
implementation = platform.python_implementation()
gil_probe = getattr(sys, "_is_gil_enabled", None)
gil_enabled = gil_probe() if gil_probe else implementation == "CPython"
emit(
"runtime",
"config",
"runtime.config",
implementation=implementation,
version=platform.python_version(),
gil=gil_enabled,
)
operations = [instruction.opname for instruction in dis.get_instructions(doubled_total)]
emit(
"bytecode",
"result",
"runtime.bytecode",
operations=" → ".join(operations[:12]),
)
frame = sys._getframe()
visible_locals = ", ".join(sorted(name for name in frame.f_locals if not name.startswith("_")))
emit(
"frame",
"state",
"runtime.frame",
functionName=frame.f_code.co_name,
locals=visible_locals,
)
left = CycleNode()
right = CycleNode()
left.peer = right
right.peer = left
left_ref = weakref.ref(left)
right_ref = weakref.ref(right)
alive_before = left_ref() is not None and right_ref() is not None
del left, right
collected = gc.collect()
alive_after = left_ref() is not None or right_ref() is not None
emit(
"gc",
"result",
"runtime.gc",
collected=collected,
aliveBefore=alive_before,
aliveAfter=alive_after,
)
asyncio.run(run_asyncio_round())
emit("result", "result", "runtime.result")
SCENARIOS = {
"syntax": run_syntax,
"semantics": run_semantics,
"runtime": run_runtime,
}
def main() -> None:
scenario = sys.argv[1] if len(sys.argv) > 1 else ""
runner = SCENARIOS.get(scenario)
if runner is None:
raise SystemExit(f"unknown scenario: {scenario}")
runner()
if __name__ == "__main__":
main()
The scenario instruments its own trace: Python calls emit(...) and prints ordered events as JSON Lines, while Node records a timestamp as each line arrives. Source and lane labels come from application code, not a CPython/asyncio profiler. The Node bridge reads output without a shell, bounds runtime and volume, and forwards events into the HTTP stream.
Practical patterns worth keeping nearby
Compare the goal, code, and caveats instead of memorizing syntax without a model.
Core collections
Tell the built-in containers apart.
numbers = [1, 2, 3] # list
point = (10, 20) # tuple
user = {"id": 7, "name": "Ada"} # dict
roles = {"admin", "editor"} # set- Empty {} creates a dictionary; use set() for an empty set.
- Nested mutable objects remain mutable inside a tuple.
Safe dictionary access
Distinguish a missing key from valid zero.
discount = payload.get("discount")
if discount is None:
discount = 10
quantity = payload["quantity"]- get fits an optional key.
- [] fits a required contract that should fail when absent.
Loop without a manual counter
Read enumerate and pair unpacking.
for index, order in enumerate(orders, start=1):
print(index, order["id"])- enumerate lazily yields (index, value) tuples.
- The pair is unpacked on every iteration.
Comprehension
Recognize map and filter in Python form.
paid_ids = [
order["id"]
for order in orders
if order["status"] == "paid"
]- The result expression is written before for.
- Prefer a normal loop when the logic becomes complex.
Function parameters
Understand positional, default, and keyword-only arguments.
def connect(host: str, port: int = 5432, *, timeout: float = 2.0):
return f"{host}:{port}; timeout={timeout}"
connect("db", timeout=1.5)- Arguments after * must be named.
- Annotations do not force CPython to check values.
Module entry point
Avoid starting the application on import.
def main() -> None:
print("start")
if __name__ == "__main__":
main()- Direct execution sets __name__ to __main__.
- Import creates definitions without calling main.
How a learning mistake becomes an incident
A realistic service: the original code, observable failure, corrected implementation, and why the correction works.
An explicit zero discount silently becomes a business default
A Python checkout reads JSON from an admin UI. The developer ports a familiar truthy-fallback pattern and assumes or only handles a missing value.
A valid discount=0 is falsy. The service substitutes 10%, stores the wrong price, and creates a financial mismatch without raising an exception.
def build_order(payload: dict) -> dict:
return {
"quantity": payload.get("quantity") or 1,
"discount": payload.get("discount") or 10,
"note": payload.get("note") or "generated",
}or returns its right operand for every falsy left operand. It does not distinguish a missing key, None, zero, and intentionally empty text.
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class OrderInput:
quantity: int
discount: int
note: str | None
def build_order(payload: dict) -> OrderInput:
quantity = payload.get("quantity")
discount = payload.get("discount")
if quantity is None:
quantity = 1
if discount is None:
discount = 10
if quantity < 1 or not 0 <= discount <= 100:
raise ValueError("invalid order values")
return OrderInput(quantity, discount, payload.get("note"))is None separates absence from valid zero. Explicit constraints reject invalid ranges and the dataclass makes the result shape visible.
What the unfamiliar calls from both code samples actually do.
payload.get("discount")- Returns a mapping value or None when the key is absent; unlike subscription, it does not raise KeyError.
value or fallback- Returns value when truthy and fallback otherwise, including for zero, empty text, None, and empty collections.
value is None- Tests identity with the None singleton without conflating absence with other falsy values.
@dataclass(...)- Generates common data-class methods; frozen restricts field assignment and slots changes instance layout.
raise ValueError(...)- Creates and raises an invalid-value exception so a broken input contract cannot continue through normal flow.
Common misconceptions
The myth is on the left; the accurate model is on the right.
Indentation is only formatting style.
Indentation is part of the grammar and defines block boundaries.
range(5) creates a list.
range creates a compact iterable; list(range(5)) materializes a list.
dict.key is equivalent to object.key.
A normal dict uses data["key"] or data.get("key"); attributes follow another protocol.
if value checks only true or false.
Truth testing also considers None, numeric zero, and empty collections false.
A tuple is the Python replacement for const.
JS const blocks rebinding while tuple blocks mutation of its sequence; these are different guarantees.
Type hints perform runtime validation.
Annotations are metadata unless a type checker or runtime library consumes them.
Explain it in your own words
If you can explain the answer without quoting documentation, your mental model is starting to take shape.
- Why does orders_copy = orders not copy the list?
- Why is data.get("count") or 10 wrong for count=0?
- What does a list comprehension create?
- Why is is None preferable to == None?
- What runs when a module is imported for the first time?