Processes and threads
Running python count.py starts a process: one program in action, with its own private memory. Line 2, count = 0, puts the variable count into that memory.
The work inside a process is done by a thread: a worker that follows your code one line at a time. Every process starts with one, the main thread. It just ran line 2.
Line 6 makes a Thread that will run work(). On line 7, t.start() sets it going. Now two threads live in one process, and both read and write the same count.
The GIL
But CPython runs Python code in only one thread at a time. The rule is the GIL, the global interpreter lock: a thread must hold it to run. The main thread holds it; t waits.
While a thread waits, the running one is asked to hand over the GIL after a short time slice, 5 ms by default. The threads take turns, so only one of the two cores is busy.
Thread t runs work(). Line 5 reads count, adds 1 and writes it back: count is now 1. The main thread sees the new value at once, because threads share their process's memory.
The GIL comes back and the main thread runs its own work() from line 7: count becomes 2. On line 8, t.join() waits until t has finished, then print(count) shows 2.
Run it again with unlucky timing. count += 1 is several bytecode instructions: a read, an add and a write. If the GIL switches between read and write, both threads read 0 and write 1. One update is lost. A Lock prevents it.
Threads still pay off when they wait. A thread that sleeps or waits for a file or the network releases the GIL, so another thread runs meanwhile. Threads suit I/O-bound work.
More processes
A fresh program, count = 0. For CPU-heavy work, use multiprocessing. Each child is a whole new Python process with its own interpreter, its own memory and its own GIL, so two processes keep both cores busy.
Run work() in a child process instead of a thread: the child adds 1 to its own copy of count, but the parent's count stays 0. Send results back through a Queue or a Pipe.
Try it yourself
;; step 1/11 · line 2: count = 0 $ python count.py # the OS starts one process >>> import os >>> os.getpid() # every process has an id 48213 # yours will differ
The GIL belongs to CPython, the standard Python from python.org, not to the Python language. Since Python 3.13 an optional free-threaded build of CPython can run without the GIL (PEP 703); the usual build still has it. multiprocessing starts children with spawn on Windows and macOS and with forkserver on Linux since Python 3.14. Both import your file again in the child, which is why the start code sits under if __name__ == "__main__":.