No history yet

Execution and Memory

From Code to Execution

When you run a Python script, it feels instantaneous. You type python my_script.py, and things just happen. But behind the scenes, your human-readable code undergoes a transformation before the computer can execute it. The standard implementation of Python, known as CPython, doesn't directly interpret your source code line by line. Instead, it first compiles it into a more efficient, intermediate format.

This intermediate format is called bytecode. It's a set of instructions that are not as low-level as machine code for a CPU, but are much more efficient for an interpreter to process than your original .py file.

This bytecode is then executed by the Python Virtual Machine (PVM). Think of the PVM as a program specifically designed to understand and run Python bytecode. It's the runtime engine of Python. This two-step process—compilation to bytecode, then execution by a VM—is why you see those .pyc files in a __pycache__ directory. Python caches the bytecode so it doesn't have to re-compile the source code every time you run it, speeding up startup for larger applications.

How Python Manages Memory

In Python, everything is an object. An integer, a string, a list, even a function—they are all objects in memory. A variable isn't a box that holds a value; it's just a name, a label that points to an object. When you write x = 100, you create an integer object with the value 100 and make the name x refer to it. If you then write y = x, you aren't copying the value. You're just creating a second name, y, that points to the exact same integer object.

This model raises a question: when is an object's memory freed? Python's main strategy for this is reference countings. Every object in memory keeps a count of how many names (variables) are pointing to it. When this count drops to zero, CPython knows the object is no longer in use and can be deallocated, freeing up the memory.

Reference counting is fast and simple, but it has one major weakness: circular references. This happens when two or more objects refer to each other. For example, object A has a reference to object B, and object B has a reference back to object A. Even if no other variables in your program point to A or B, their reference counts will each be 1, so they'll never be deleted. This creates a memory leak.

Cleaning Up the Mess

To solve the problem of circular references, Python employs a supplementary mechanism: a garbage collectors. The garbage collector's job is to periodically scan for and clean up objects that reference counting missed.

Python uses a 'generational' garbage collector. It's based on the observation that most objects are short-lived. The collector groups objects into three 'generations'. New objects start in generation 0. If an object survives a garbage collection cycle in its generation, it gets promoted to the next, older generation.

Generation 0 is scanned most frequently. Generations 1 and 2 are scanned less often. This approach is efficient because the collector spends most of its effort on the part of memory where garbage is most likely to be found—the newest objects.

The Lock on Parallelism

Memory management in a multi-threaded environment is complex. If two threads try to modify an object's reference count at the same time, you could end up with a corrupted count, leading to premature deletion or memory leaks. CPython's solution is the (GIL).

The GIL is a mutex (a type of lock) that ensures only one thread can execute Python bytecode at a time within a single process. This simplifies CPython's implementation and memory management immensely, as it prevents race conditions for object access. However, it comes at a cost.

Lesson image

Because of the GIL, a multi-threaded Python program cannot take full advantage of multi-core processors for CPU-bound tasks. If your code is spending all its time performing calculations, running it across four threads on a four-core machine won't be four times faster. In fact, it might even be slightly slower due to the overhead of managing the threads.

However, for I/O-bound tasks—like waiting for network requests, reading from a disk, or querying a database—the GIL is released. This allows another thread to run, making threads very effective for concurrent I/O operations.

Quiz Questions 1/5

What is the first step CPython takes when executing a .py script?

Quiz Questions 2/5

After executing the code x = [10, 20] and y = x, what is the relationship between x and y?

Understanding how Python works under the hood is key to writing efficient, robust code. Knowing about bytecode, memory management, and the GIL helps you reason about performance and make informed architectural decisions.