Skip to content

Visualize Python

Each picture runs the code beside it, one step at a time. Type your own lines under it and watch again. All visualizers.

Paste any function and write the call to watch. Each step shows the line, what changed, the calls in progress and the answer. Lists show index pointers (nums[i] marks i), dicts show as tables, and classes like ListNode and TreeNode show as diagrams.

Paste a function, then watch it run

Ctrl + Enter visualizes

Runs in this browser. Loads about 6 MB the first time, best on Wi-Fi.

Under every code editor in the lessons, Visualize does the same with the first test.

A slice works out where it starts and stops, counting a negative place from the back. Then it takes a place at a time, by its step. The stop is never taken. Type your own text and slice under the picture, like -3: or ::2. The lesson: My shop’s total says 0.30000000000000004.

A slice backwards
  1. s0-8
  2. t1-7
  3. r2-6
  4. e3-5
  5. s4-4
  6. s5-3
  7. e6-2
  8. d7-1

Step 1 of 12. s holds 8 characters. Each has a place counted from the front, 0 to 7, and one counted from the back, -8 to -1, shown under it.

s = "stressed"
print(s[::-1])

Try your own input

Numbers go into a set one at a time, each to the slot its hash picks, then one is looked up. A list would compare it with every item instead. Type your own numbers under the picture. The lesson: My import got 100 times slower when the data grew 10 times.

Where a set looks

The set’s table: 8 slots

  1. Slot 0: empty
  2. Slot 1: empty
  3. Slot 2: empty
  4. Slot 3: empty
  5. Slot 4: empty
  6. Slot 5: empty
  7. Slot 6: empty
  8. Slot 7: empty

The same numbers in a list

None yet.

Step 1 of 16. seen = set() makes an empty set: a table of 8 slots, all empty.

seen = set()
for x in [3, 11, 19, 27]:
    seen.add(x)
print(27 in seen)

Try your own input

Python compiles a line to bytecode before it runs it. Each value goes on a stack, and each operator takes the top two and puts one answer back. Type your own formula and values under the picture, like a - b // c. The lesson: Python says my formula has too many nested parentheses.

A line on the stack

    Names

    a7
    b2
    c3

    The stack is empty.

    Step 1 of 7. Before any of line 2 runs, Python compiles it into 6 instructions for a stack. a, b and c already hold 7, 2 and 3.

    a, b, c = 7, 2, 3
    x = a - b // c
    # Python 3.14 runs line 2 as:
    LOAD_NAME      a
    LOAD_NAME      b
    LOAD_NAME      c
    BINARY_OP      //
    BINARY_OP      -
    STORE_NAME     x
    

    Try your own input

    A name points to an object. It doesn’t hold one. Watch two names share one list, and a copy that is new only on the outside. Then type your own lines under the picture. The lesson: Names, objects and references.

    Names pointing at objects

    Names

    none yet

    Objects

    no lists, dicts or tuples yet

    Step 1 of 8. Before the first line runs, there are no names and no objects.

    a = [1, 2]
    b = a
    b.append(3)
    c = a.copy()
    c.append(4)
    del a
    print(b, c)
    

    Try your own input

    Use numbers, text, lists, dicts and tuples. It knows =, +=, del, print(), comparisons, slices, comprehensions, the common list methods, sorted(), list(), dict(), tuple(), range(), and copy.deepcopy() after import copy.

    Each call of a function gets a box for its names, drawn inside the box of the function it was made in. Watch Python look for a name box by box, and see a finished call’s box stay while a function made in it still uses it. The lesson: Python says my counter has no value.

    Two counts, in two boxes

    Built-inPython’s own names

    • print
    • len
    • max

    Globalrunning now

    no names yet

    Step 1 of 12. Before the first line runs, the global box is empty. The built-in box holds names like print and len.

    count = 0
    def make_counter():
        count = 10
        def tick():
            nonlocal count
            count += 1
            return count
        return tick
     
    orders = make_counter()
    print(orders(), count)
    

    Try your own input

    Indent with spaces. It knows def, return, global, nonlocal, pass, =, +=, -=, calls, whole numbers, text, True, False, None, lists with [i], .append() and .clear(), and the built-ins print, len, max, min, abs, sum and str.

    A for loop keeps its place in a list by position. So removing a reading moves the next one back into a place the loop has done. A comprehension builds a new list instead, and runs its parts in an order you might not expect. The lesson: I removed the bad readings, but one is still there.

    The loop skips a reading
    1. 30
    2. -41
    3. -22
    4. 83

    Step 1 of 10. readings holds 4 readings. 2 are below 0: errors. The loop should remove them.

    readings = [3, -4, -2, 8]
    for r in readings:
        if r < 0:
            readings.remove(r)
    print(readings)
    

    Try your own input

    A comprehension keeps what passes
    1. 30
    2. -41
    3. -22
    4. 83

    kept = []

    Step 1 of 13. The comprehension starts a new, empty list. It reads readings, but never changes it.

    readings = [3, -4, -2, 8]
    kept = [
        r
        for r in readings
        if r >= 0
    ]
    print(kept, readings)
    

    Try your own input

    When a line raises an error, Python goes back through the calls in progress, newest first, until a try with a matching except catches it. A call with no try just stops. Type a port that isn’t a number, or one out of range, and watch where the error is caught. The lesson: My server ignored the port I set.

    A bad port goes up to the except
    1. Top levelrunning now

    Step 1 of 11. ConfigError is a new kind of error, made from ValueError, so a ConfigError is a ValueError too. Lines 4 to 14 only define functions: nothing in them runs yet.

    class ConfigError(ValueError):
        pass
     
    def read_port(text):
        try:
            port = int(text)
        except ValueError as err:
            raise ConfigError(f"not a number: {text!r}") from err
        if not 1 <= port <= 65535:
            raise ConfigError(f"out of range: {port}")
        return port
     
    def load_config(settings):
        return {"port": read_port(settings["port"])}
     
    try:
        print(load_config({"port": "8O80"}))
    except ConfigError as err:
        print("Bad config:", err)
    

    Try your own input

    A path that doesn’t start with / starts in the folder you ran the program from, not where the script is. Pick a folder to run from and a path, and follow it one part at a time: .. goes up a folder, and each name must be there. The lesson: My script can’t find its file when I run it from another folder.

    Where a path leads

    The path, part by part

    1. ..
    2. data
    3. prices.csv
    • /
    • home/
    • sam/
    • shop/
    • data/
    • prices.csv
    • tools/you ran it from here
    • report.pythe script

    Step 1 of 7. You ran python /shop/tools/report.py from /shop/tools. That’s the program’s current folder, the one Path.cwd() gives.

    # In a terminal, in /shop/tools:
    #   python /shop/tools/report.py
    # In report.py:
    with open("../data/prices.csv", encoding="utf-8") as f:
        first = f.readline()
    

    Try your own input

    A generator hands over one item when asked, then waits. Chained together, they read a file one line at a time, so even a 10 GB log needs almost no memory. The lesson: Count errors in a 10 GB log.

    A pipeline of generators

    app.log

    1. 09:00:01 INFO user 17 logged in (not read yet)
    2. 09:00:02 ERROR payment timed out (not read yet)
    3. 09:00:04 WARN slow query (not read yet)
    4. (blank line) (not read yet)
    5. 09:00:07 ERROR payment timed out (not read yet)
    1. read_lineswaiting
    2. parsewaiting
    3. only_errorswaiting
    4. sumwaiting

    In memory now

    Nothing yet

    count = 0

    Step 1 of 19. sum asks only_errors for an error. only_errors asks parse, and parse asks read_lines. Nothing has been read yet.

    from collections.abc import Iterable, Iterator
     
    Entry = tuple[str, str, str]  # (time, level, message)
     
    def read_lines(path: str) -> Iterator[str]:
        with open(path, encoding="utf-8", errors="replace") as f:
            yield from f
     
    def parse(lines: Iterable[str]) -> Iterator[Entry]:
        for line in lines:
            parts = line.split(maxsplit=2)
            if len(parts) < 2:
                continue  # a blank or cut-off line
            message = parts[2].rstrip() if len(parts) == 3 else ""
            yield parts[0], parts[1], message
     
    def only_errors(entries: Iterable[Entry]) -> Iterator[Entry]:
        for entry in entries:
            if entry[1] == "ERROR":
                yield entry
     
    def count_errors(path: str) -> int:
        return sum(1 for _ in only_errors(parse(read_lines(path))))
    

    Try your own input