RRUNTIME LABruntime observatoryNNEON · Articles in 90 languages
CHAPTER 22
DEEP DIVE · FROM BASICS TO CODE

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.

FIRST, IN PLAIN LANGUAGE

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.

TECHNICAL FOUNDATION

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.

Why it mattersFalse analogies are the main risk for a JS developer: dict resembles object but is a mapping; list resembles Array but slicing and comprehensions change common patterns; None is not undefined; and is tests identity rather than value.
WHERE THE WORK RUNS
01PYTHON SOURCE.py · statements · expressions
02PARSERtokens · AST · scopes
03CPYTHONcode objects · bytecode · frames
04OBJECTStypes · collections · stdlib
01 · GLOSSARY

Terms used in this experiment

Understand the words first, then the execution order.

01

Name

An identifier bound to an object. Reassignment changes the binding rather than the type of a dedicated variable cell.

02

Statement

An instruction such as if, for, import, return, or assignment that controls execution.

03

Expression

A fragment evaluated into an object value, such as a call, arithmetic expression, comprehension, or subscription.

04

Iterable

An object whose items can be requested one by one by for, a comprehension, sum, list, and other consumers.

05

list

A mutable ordered sequence. It resembles a JS Array, but its methods and copy model differ.

06

tuple

An immutable sequence commonly used for a fixed group of values and unpacking.

07

dict

A key-to-value mapping. Keys must be hashable; [] raises KeyError for a missing key while get returns a fallback.

08

set

A collection of unique hashable items with fast membership tests and set operations.

09

None

The singleton object representing no value, normally compared with is None.

10

Comprehension

An expression that builds a list, dictionary, or set from an iterable with transformation and optional filtering.

02 · MECHANICS

What happens step by step

Each step maps to an observable runtime state.

  1. 01
    The interpreter loads a module

    Top-level statements execute in order, so import is not textual inclusion and may have side effects.

  2. 02
    Indentation forms a block

    A colon opens a suite after if, for, def, class, try, with, or match; equal indentation shows membership.

  3. 03
    Assignment binds a name

    orders = [...] creates a list and binds orders to it. Another name may reference the same list.

  4. 04
    for requests items

    for order in orders receives objects from the iterable. Use enumerate only when an index is genuinely needed.

  5. 05
    A comprehension builds a collection

    The left expression transforms, for chooses the source, and a trailing if filters.

  6. 06
    def creates a function

    The body does not run at definition time. Parameters bind to passed objects on a call.

  7. 07
    return ends the call

    Without an explicit return a function returns None; several comma-separated values form a tuple.

03 · CONTEXT

Where the result needs context

These details explain why similar code can sometimes produce a different trace.

01

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.

02

== and is ask different questions

== checks value equality while is checks object identity. Use is for None and private sentinel objects.

03

A slice is normally a shallow copy

items[:] creates a new list that still contains references to the same nested objects.

04

dict is not a JavaScript object

Keys may have multiple hashable types, missing [] raises KeyError, and attribute access is a different protocol.

05

Annotations do not validate runtime values

name: str helps an IDE and type checker, but ordinary CPython does not enforce the binding type.

06

Loops may have else

The else branch runs when the loop finishes without break; it does not belong to the nearest if.

07

Scope is not block-based

if and for do not create local scopes. Modules, functions, classes, and comprehensions normally do.

01
Theory

First understand which parts of Node participate in execution.

02
Simplified code

Then remove instrumentation and focus on the central mechanism.

03
Runtime code

Finally match the model to the code that produces the live trace.

04 · Simplified code

A minimal model without instrumentation

src/python-lab.py · educational snippetPython
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}"
05 · Runtime code

The complete code executed by the scenario

This is not an alternative example: these are the functions and files used by the Run button.

ACTUAL SOURCE

The first file is the real CPython scenario; the second safely starts the child process and turns its JSON Lines into the live trace.

src/python-lab.py
CPython scenario294 lines
#!/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.

06 · RECIPES

Practical patterns worth keeping nearby

Compare the goal, code, and caveats instead of memorizing syntax without a model.

01

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.
02

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.
03

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.
04

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.
05

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.
06

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.
07 · PRODUCTION CASES

How a learning mistake becomes an incident

A realistic service: the original code, observable failure, corrected implementation, and why the correction works.

CASE 01

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.

INCIDENT CONTEXT

A valid discount=0 is falsy. The service substitutes 10%, stores the wrong price, and creates a financial mismatch without raising an exception.

BEFOREPROBLEMATIC IMPLEMENTATION
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.

AFTERCORRECTED IMPLEMENTATION
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.

FUNCTIONS AND CONSTRUCTS

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.
WHY THE CORRECTION WORKS

When reading Python, ask what truthiness means at that boundary. An or fallback is appropriate only when every falsy value is genuinely equivalent to absence.

WHAT PRODUCTION SHOWED
  • Orders with an explicit discount=0 are stored with discount=10.
  • No exception or validation error appears, so only business metrics reveal the defect.
  • The mismatch is concentrated in payloads containing zero values.
08 · DO NOT CONFUSE

Common misconceptions

The myth is on the left; the accurate model is on the right.

MYTH

Indentation is only formatting style.

ACTUALLY

Indentation is part of the grammar and defines block boundaries.

MYTH

range(5) creates a list.

ACTUALLY

range creates a compact iterable; list(range(5)) materializes a list.

MYTH

dict.key is equivalent to object.key.

ACTUALLY

A normal dict uses data["key"] or data.get("key"); attributes follow another protocol.

MYTH

if value checks only true or false.

ACTUALLY

Truth testing also considers None, numeric zero, and empty collections false.

MYTH

A tuple is the Python replacement for const.

ACTUALLY

JS const blocks rebinding while tuple blocks mutation of its sequence; these are different guarantees.

MYTH

Type hints perform runtime validation.

ACTUALLY

Annotations are metadata unless a type checker or runtime library consumes them.

09 · SELF-CHECK

Explain it in your own words

If you can explain the answer without quoting documentation, your mental model is starting to take shape.

  1. Why does orders_copy = orders not copy the list?
  2. Why is data.get("count") or 10 wrong for count=0?
  3. What does a list comprehension create?
  4. Why is is None preferable to == None?
  5. What runs when a module is imported for the first time?