Skip to content

An interactive reference for full-stack interviews

How Node.js and Python actually run your code

Five explorers, drawn as lines on a transit map. Step through real programs one line at a time and watch the call stack, the heap, the GIL and the type system move. Every output was checked against real Node.js and CPython, and every line ends with the interview questions it prepares you for.

A route for interview prep

Each line builds on the one before it: async first, then values and equality, then how Python stores objects, how its types interact, and finally concurrency.

NNode event loop

How one thread juggles thousands of tasks: the call stack, the nextTick and promise queues, timers, the libuv phases and the thread pool.

You’ll be ready for
  • Does a promise callback run before setTimeout(fn, 0)?
  • setImmediate or setTimeout: which fires first?
  • What blocks the event loop, and how do you fix it?

MPython memory

Names, objects and reference counts, how the cycle collector frees what refcounting can’t, and why pymalloc keeps memory after you delete things.

You’ll be ready for
  • Is Python pass-by-value or pass-by-reference?
  • What’s the difference between is and ==?
  • Why doesn’t memory drop after del?

GThe GIL

Four threads and one lock on a live timeline: CPU-bound versus I/O-bound work, free-threaded Python, processes, and a race condition you can step through.

You’ll be ready for
  • Do threads make Python code faster?
  • Does the GIL make my code thread-safe?
  • Threads, processes or asyncio: how do you choose?

JJavaScript types

The typeof junction, how V8 stores each type, copies versus references, the exact == algorithm, truthiness and the 64 bits inside a number.

You’ll be ready for
  • Why is [] == false true?
  • Shallow copy or deep copy?
  • Why is 0.1 + 0.2 !== 0.3?

PPython types

The built-in types and where to use each one, how a + b is dispatched, when += changes things in place, and how dict keys are hashed.

You’ll be ready for
  • What makes an object hashable?
  • What happens when Python evaluates a + b?
  • What’s wrong with def f(items=[])?

Step through it

Play, Next and Back move one line of code at a time. Use ← and → to step, Space to play, and pick a speed that suits you.

Outputs you can trust

Every printed output was checked against Node.js 22 and CPython 3.12. The JavaScript passes ESLint and the Python is formatted with black.

Built for revision

Each line ends with interview questions and short answers you can say out loud. Link straight to a line with #node, #memory, #gil, #js-types or #py-types.

The Node.js event loop, one stop at a time

Pick a scenario on the line below, then press Play or step with the arrow keys. Every callback travels from your code through Node’s queues onto the call stack, so you can see exactly why the output lands in that order.

Classic order

Not started
script.js CommonJS
    Call stack one thread
    Node APIs and libuv
    clock0ms
    Timers
    Thread pool 4 threads by default
    Output
      Microtask queues drained whenever the stack empties
      Nprocess.nextTick queue
      PPromise microtask queue
      Event loop one iteration, left to right
        back to timers for the next iteration

        The rules behind every run

        Every scenario above follows the same five rules, in this order.

        1. Run the script to the endSynchronous code always finishes first. Nothing interrupts it: not timers, not I/O.
        2. Drain the microtasksAll process.nextTick callbacks, then all promise callbacks. Repeat until both queues are empty.
        3. Go round the looptimers, pending callbacks, idle/prepare, poll, check, close callbacks. Each phase runs the callbacks waiting in its queue.
        4. Drain microtasks after every callbackSince Node 11, a promise or nextTick created inside a callback runs before the next callback of the same phase.
        5. Wait, or exitPoll blocks while I/O is still pending. When no timers, handles or requests are left, the process exits.

        ES modules are different. In an .mjs file, or with "type": "module", the top level already runs inside a promise job, so promise callbacks run before nextTick. The classic example prints A E C D B there.

        Where each API sends its callback

        Use this as a route map when you read unfamiliar async code: find the API, and you know which queue its callback waits in.

        Interview questions: the Node.js event loop

        Short answers you can say out loud. Try answering each question yourself before you open it.

        Node is single-threaded. How does it handle thousands of connections?

        Your JavaScript runs on one thread, but the waiting doesn’t. Node hands network I/O to the operating system (epoll, kqueue, IOCP) and file-system work to libuv’s thread pool, then runs your callback on the main thread when the result is ready. The main thread never waits; it only runs callbacks.

        What exactly is the event loop?

        A loop inside libuv that cycles through phases: timers, pending callbacks, poll (I/O), check (setImmediate) and close callbacks. Each phase has a queue, and each callback runs to completion on the call stack before the next one starts.

        In what order do synchronous code, process.nextTick, promises and timers run?

        Synchronous code first. Then the whole nextTick queue, then the whole promise microtask queue. Only then does the loop move on to setTimeout callbacks in the timers phase and setImmediate in the check phase. Both microtask queues are drained again after every single callback.

        Why do promises run before process.nextTick in an ES module?

        Node starts an ES module from inside a promise job, so when its top-level code finishes, V8 is still draining the microtask queue and runs the promise callbacks you queued before Node gets to the nextTick queue. In CommonJS the nextTick queue goes first. The examples on this line are CommonJS: run scenario 1 as an .mjs file and C and D swap places.

        setTimeout(fn, 0) or setImmediate(fn): which runs first?

        From the main module it isn’t guaranteed; it depends on how quickly the loop starts. Inside an I/O callback, setImmediate always wins, because the check phase comes straight after the poll phase.

        What does the libuv thread pool do, and how big is it?

        It runs work that has no non-blocking OS API: most fs calls, dns.lookup, and CPU-heavy crypto and zlib functions. It has 4 threads by default, set by UV_THREADPOOL_SIZE. Network sockets don’t use it.

        What blocks the event loop, and how do you fix it?

        Any long synchronous work: a huge JSON.parse, sync fs or crypto calls, heavy loops, a regex with catastrophic backtracking. While it runs, no request, timer or promise can move. Split the work into chunks, stream the data, or move it to worker_threads.

        Why did my 10 ms timer fire after 100 ms?

        A timer’s delay is a minimum, not a promise. The callback runs in the first timers phase after the delay has passed, so a long callback ahead of it delays it, exactly like the loop done at 100 ms scenario on this line.

        Can process.nextTick starve the event loop?

        Yes. The nextTick queue is drained completely before the loop continues, so a callback that keeps scheduling another nextTick stops all I/O. setImmediate yields to I/O instead.

        When would you use worker_threads, and when cluster?

        worker_threads run JavaScript in parallel inside one process and can share memory with SharedArrayBuffer; use them for CPU-heavy tasks. cluster, or several processes behind a load balancer, runs separate Node processes so a server can use every core.

        Outputs verified with Node.js 22 running CommonJS files. Since libuv 1.45 (Node 20 and later), the timers step technically runs at the end of each iteration, after close callbacks; the order of phases shown here is the same.

        Python memory, drawn as a map

        Names on the left, objects on the heap to the right, and every arrow is a reference. Step through four short programs to see when objects are shared, counted, and freed, then play with the allocator that stores them.

        Names point to objects

        demo.py CPython 3.12+
          Output
            Namespaces and heap arrows are references
            1. Copy counts into gc_refs
            2. Subtract references inside the group
            3. Find what’s unreachable
            4. Free it

            Where small objects actually live

            CPython doesn’t ask the operating system for memory every time you create an object. Objects of 512 bytes or less come from pymalloc: big arenas split into pools, and each pool hands out fixed-size blocks for one size class (multiples of 16 bytes). Allocate and free some objects and watch what happens to the arenas.

            System malloc objects over 512 bytes skip pymalloc

            Scaled down for the screen: a real pool holds hundreds of blocks and a real arena holds many pools. Sizes are sys.getsizeof() on 64-bit CPython 3.12, rounded up to the next 16-byte size class.

            What to remember

            Names are referencesAssignment, argument passing and return copy references, never objects. Mutations are visible through every name.
            Counting frees most objectsWhen an object’s count reaches 0 it is freed on the spot, deterministically. del only removes a name.
            The GC is for cyclesObjects that refer to each other keep their counts above 0. The cyclic collector finds and frees them.
            Freed isn’t returnedFreed blocks go back to pymalloc, not the OS. An arena is only released when every pool in it is empty.

            Interview questions: Python memory

            Short answers you can say out loud. Try answering each question yourself before you open it.

            What happens in memory for a = [1, 2] followed by b = a?

            One list object is created on the heap and both names refer to it. Assignment never copies: it binds a name to an object, and the list’s reference count goes to 2.

            Is Python pass-by-value or pass-by-reference?

            Neither. It passes references to objects by value, often called call by sharing. A function can mutate an object you pass in and you’ll see the change, but rebinding the parameter inside the function doesn’t touch your variable.

            How does CPython free memory?

            Mainly by reference counting: when an object’s count drops to zero it’s freed immediately. A generational cyclic garbage collector finds groups of objects that only reference each other, which refcounting alone can never free.

            What’s the difference between is and ==?

            is checks identity: are these the same object? == checks equality through __eq__. Use is for singletons like None, and == for everything else.

            Why can x is y be True for two equal small ints but not for big ones?

            CPython caches the integers from -5 to 256, so those are always the same objects. Whether other equal numbers or strings share an object depends on interning and constant folding, an implementation detail you should never rely on.

            Does del x free the object?

            Not directly. del removes a name (or a container item) and decrements the reference count. The object is freed only when its count reaches zero, or later by the garbage collector if it’s part of a cycle.

            Why doesn’t my process shrink after I delete a big list?

            Small objects come from pymalloc, which takes memory from the OS in large arenas and can only give an arena back when every block in it is free. A few surviving objects keep whole arenas alive, as the pymalloc demo on this line shows. Python reuses that memory; it just doesn’t hand it back.

            How do you track down a memory leak in Python?

            Look for references that live too long: module-level caches, an unbounded functools.lru_cache, closures and callbacks that capture big objects, and globals. tracemalloc shows which lines allocated the memory that stays, and gc.get_referrers() shows who is holding an object.

            When would you use weakref?

            For caches and back-references that shouldn’t keep an object alive. A weak reference doesn’t increase the reference count, so the object can still be freed, after which the weak reference returns None.

            Counts on the map include only references created by the program. sys.getrefcount() reports one more (its own argument), and immortal objects report a huge fixed value.

            Python’s GIL, on a timeline

            Four threads, one Global Interpreter Lock. Pick a workload and a Python build, press Run, and watch who holds the lock, what the CPU cores are doing, and how long the whole job takes.

            Four threads, one interpreter

            Ready
            Workload
            Python build
            5 ms
            Threads one interpreter, one GIL
            CPU cores lit while running Python code
            Timeline time in simulated milliseconds
            running Python code waiting for the GIL waiting on I/O (GIL released) starting a process

            0.0 ms

            The GIL doesn’t make your code thread-safe

            The GIL guarantees that one bytecode instruction runs at a time, not that your read, modify and write steps stay together. Step through two deposits into one balance, with and without a lock.

            Without a lock

            race.py
              Output
                Shared state and threads
                balance
                Main thread
                T1: deposit(50)
                current
                T2: deposit(30)
                current

                Which tool for which job

                Waiting on I/O

                Network calls, disk, databases, subprocesses. The GIL is released while a thread waits, so threads overlap well.

                threading, asyncio

                CPU-heavy pure Python

                Loops over Python objects. One GIL means one core, so use separate processes, or a free-threaded build (python3.14t) with thread-safe code.

                multiprocessing, ProcessPoolExecutor

                NumPy and C extensions

                Many extensions release the GIL during heavy work (NumPy, hashlib, zlib), so plain threads can use several cores.

                threads around C-level work

                Shared mutable state

                With or without a GIL, a read followed by a write can interleave. Protect it, or pass messages instead of sharing.

                Lock, queue.Queue

                Free-threaded Python. CPython 3.13 added an experimental build without the GIL, and in 3.14 it became officially supported (PEP 779). Check with sys._is_gil_enabled(); setting PYTHON_GIL=1 turns the lock back on, and importing an extension that isn’t marked safe for it re-enables the GIL automatically.

                Interview questions: the GIL and concurrency

                Short answers you can say out loud. Try answering each question yourself before you open it.

                What is the GIL?

                The Global Interpreter Lock is a mutex in CPython that lets only one thread run Python bytecode at a time within an interpreter. It keeps the interpreter’s internals, such as reference counts, safe without fine-grained locking.

                Then why use threads in Python at all?

                Because a thread releases the GIL while it waits: blocking I/O on sockets and files, and time.sleep, let other threads run. Many C extensions, such as NumPy and hashlib on large inputs, release it during heavy computation too.

                How do you speed up CPU-bound Python code?

                Use processes (multiprocessing or concurrent.futures.ProcessPoolExecutor) so each worker has its own interpreter and GIL, move the hot loop into native code that releases the GIL, or run on the free-threaded build.

                Does the GIL make my code thread-safe?

                No. Threads can switch between any two bytecodes, so count += 1 (load, add, store) can interleave and lose updates, which is the race you can step through on this line. Protect shared state with threading.Lock, or pass data between threads with queue.Queue.

                How often do threads switch?

                A thread waiting for the GIL asks for it after the switch interval, 5 ms by default (sys.getswitchinterval()), and the holder releases it at the next safe point. A thread that starts blocking I/O releases it straight away.

                What is free-threaded Python?

                A separate CPython build without the GIL (PEP 703), added experimentally in 3.13 as python3.13t and officially supported from 3.14 (PEP 779). Threads can then run Python code on several cores at once, at some cost to single-threaded speed. sys._is_gil_enabled() tells you which mode you’re in.

                asyncio, threads or processes: how do you choose?

                asyncio for many concurrent I/O tasks with async libraries on a single thread. Threads for I/O with blocking libraries, or a handful of background tasks. Processes for CPU-bound pure Python, paying for start-up time and for pickling data between them.

                What is the convoy effect?

                A CPU-bound thread keeps the GIL for a whole switch interval at a time, so I/O threads that only need it for a moment after each read queue up behind it. Their latency grows even though the total work barely changes, as the Mixed workload on this line shows.

                The simulator is a simplified model of CPython’s GIL: a waiting thread asks for a switch after one interval (5 ms by default), the holder drops the lock, and waiters are served first come, first served. Real runs add operating-system noise, so the shapes are right and the numbers are illustrative. Free-threaded runs add 7% per-thread overhead; process start-up is shown as 10 ms.

                JavaScript types, sorted at the junction

                Every JavaScript value is one of seven primitive types or an object. Send values through the typeof junction, flip the cards to see how V8 stores them, trace exactly what == does, and take a number apart bit by bit.

                The typeof junction

                0 of 15 sent

                  Pick a value to send it through typeof. There are only eight possible answers.

                  Seven primitives and one object type

                  Primitives are immutable and compared by value; objects are mutable and compared by reference. Flip a card to see how V8 represents that type.

                  Stack or heap? You’ll often read that primitives live on the stack and objects on the heap. In V8 almost everything lives on the heap, and a variable holds a pointer to it; only small integers (Smis) fit inside the slot itself. What really separates primitives is that they can’t be changed, so sharing one is invisible.

                  Copy or share?

                  Assigning a primitive copies it. Assigning an object copies a reference. Spread copies one level deep; structuredClone copies all the way down.

                  Primitives, references and clones

                  Node.js 17+
                  copy.js
                    Output
                      Variables and heap arrows are references

                      What == actually does

                      === never converts. == follows a fixed list of conversion rules from the spec, one step at a time. Pick two values and follow the trace, or click any square in the grid.

                      [] == false

                      Left value
                      Right value
                        All 225 pairs filled means true

                        Truthy or falsy?

                        An if converts its condition with Boolean(). Exactly eight values are falsy; everything else, including some surprising ones, is truthy. Pick a value, then pick the bin you think it belongs in.

                        Sort the values

                        0 of 16 sorted

                        Pick a value first.

                        Inside a number

                        A JavaScript number is an IEEE 754 double: 1 sign bit, 11 exponent bits and 52 fraction bits. Type a value, pick a preset, or flip any bit and watch the number change.

                        0.1

                        normal number

                        Interview questions: JavaScript types

                        Short answers you can say out loud. Try answering each question yourself before you open it.

                        What are JavaScript’s types?

                        Seven primitives (undefined, null, boolean, number, bigint, string and symbol) plus objects. Arrays, functions, dates, maps and class instances are all objects.

                        What can typeof return?

                        Eight strings: "undefined", "boolean", "number", "bigint", "string", "symbol", "object" and "function". typeof null is "object" because of a historical bug, so test for null with === null and for arrays with Array.isArray().

                        == or ===?

                        Default to ===, which never converts types. == applies conversion rules: null and undefined only equal each other, booleans become numbers, a string compared with a number becomes a number, and objects are turned into primitives. The one common use of == is x == null, which checks for null or undefined.

                        Is JavaScript pass-by-reference?

                        No. Everything is passed by value, but the value of an object variable is a reference. A function can mutate an object you pass in, but reassigning the parameter doesn’t change your variable. Python works the same way.

                        Shallow copy or deep copy?

                        Spread, Object.assign and slice() copy one level, so nested objects stay shared. structuredClone copies deeply, including Map, Set, Date and circular references, but it throws on functions, and class instances come back as plain objects.

                        Why is 0.1 + 0.2 !== 0.3?

                        Numbers are 64-bit IEEE 754 doubles, and 0.1, 0.2 and 0.3 have no exact binary form, so each is stored as the nearest double and the rounding errors don’t cancel out. Compare with a tolerance, or keep money in integer cents.

                        Which values are falsy?

                        false, 0, -0, 0n, "", null, undefined and NaN (plus the legacy document.all in browsers). Everything else is truthy, including "0", "false", [] and {}.

                        What is Number.MAX_SAFE_INTEGER, and why does it matter?

                        253 − 1. Above it, not every integer can be represented, so a 64-bit ID from a database can silently change when parsed as a number. Keep such IDs as strings or BigInt.

                        null or undefined?

                        undefined means a value was never set: a missing property, an unassigned variable, a function with no return. null is an explicit “no value” that you assign. JSON has null but no undefined, and JSON.stringify drops properties whose value is undefined.

                        typeof results, conversions and truthiness follow the ECMAScript specification and match V8 in Node.js and Chrome. The copy example prints 10 20 Bob [ 'dev', 'ops' ] [ 'dev', 'ops', 'qa' ] in Node.js 22.

                        Python types, and how they get along

                        Everything in Python is an object, and its type decides what operators do with it. Explore the type network, send two operands through the operator machinery, then watch += and dict keys behave in ways that surprise most people.

                        The built-in type network

                        14 types

                        Click any station to see what that type is for, with a small example.

                        What happens when you write a + b

                        Python asks the left operand first. If its method returns NotImplemented, the right operand gets a turn with the reflected method. Pick two values and an operator; every step below was recorded from real CPython objects.

                        Operator dispatch

                        Left operand a
                        Right operand b

                          Every pair click a square to trace it

                          Each square shows the type of the result; striped red squares raise TypeError.

                          += changes things in place, or doesn’t

                          x += y first tries x.__iadd__(y). Mutable types implement it and change the object in place; immutable ones don’t, so Python falls back to x = x + y and rebinds the name.

                          Lists, strings and a tuple

                          CPython 3.12+
                          augmented.py
                            Output
                              Names and heap arrows are references

                              Dict keys and hashing

                              A dict keeps its entries in insertion order plus a small index table. hash(key) picks a slot, and keys that are equal must have equal hashes, which is exactly why 1, True and 1.0 end up as one key.

                              Five assignments, two keys

                              compact dict, 8 slots
                              keys.py
                                Output
                                  Inside d index table and entries
                                  Index table: slot to entry number
                                  Entries, in insertion order

                                  Interview questions: Python types

                                  Short answers you can say out loud. Try answering each question yourself before you open it.

                                  Which built-in types are mutable?

                                  list, dict, set and bytearray, plus most classes you write. int, float, complex, bool, str, bytes, tuple, frozenset, range and None are immutable: every “change” builds a new object.

                                  What makes an object hashable, and why does it matter?

                                  It needs a __hash__ that never changes during its lifetime, and objects that compare equal must hash equally. Only hashable objects can be dict keys or set members, which is why a list can’t be a key but a tuple of hashable items can.

                                  Why do 1, True and 1.0 end up as one dict key?

                                  They compare equal and have the same hash, so the dict treats them as one key. The first key object is kept and only the value is replaced, as the hashing stepper on this line shows.

                                  What happens when Python evaluates a + b?

                                  It calls a.__add__(b). If that returns NotImplemented, it tries b.__radd__(a), and raises TypeError if both give up. If b’s type is a subclass of a’s type that overrides __radd__, b goes first.

                                  Is x += y the same as x = x + y?

                                  Not for mutable types. += calls __iadd__ first, which extends a list in place, so every name for that list sees the change. Immutable types fall back to x = x + y and rebind only x. That’s also why pair[0] += [2] on a tuple both mutates the list and raises.

                                  What’s wrong with def f(items=[])?

                                  Default values are evaluated once, when the function is defined, so every call shares the same list. Use items=None and create a new list inside the function.

                                  Why write x is None instead of x == None?

                                  None is a singleton, so identity is the precise test. It’s also faster, and it can’t be fooled by a class whose __eq__ says yes to everything.

                                  list, tuple, set or dict: how do you choose?

                                  A list for an ordered collection you change, a tuple for a fixed record or a hashable key, a set for uniqueness and fast membership tests, and a dict for lookup by key. Reach for a frozenset when you need a set that is hashable.

                                  How do you copy a nested structure?

                                  list(x), x.copy(), x[:] and copy.copy() are shallow: the new container holds the same inner objects. copy.deepcopy() copies everything recursively.

                                  Dispatch traces were recorded by calling each dunder method on real objects in CPython 3.12. String hashes change every run unless PYTHONHASHSEED is set, so the hash shown for '1' is one possible value.

                                  Runtime lines

                                  An interactive reference for full-stack engineers preparing for interviews: how Node.js and Python actually run your code, drawn as a transit map you can step through.

                                  The lines

                                  About the author

                                  Sergiu Vlad is a senior software engineer with 15+ years of building secure ML, cloud, data and full-stack systems, working remote-first from Cluj-Napoca, Romania. He built Runtime lines as a reference for anyone preparing for a full-stack engineering role.

                                  sergiuvlad.com LinkedIn me@sergiuvlad.com

                                  © 2026 Sergiu Vlad. All outputs checked against Node.js 22 and CPython 3.12. Spotted a mistake? Let me know.