I will be using the term Python throughout this article. However, taking into account that there are different implementations of Python(Cpython, Cython, Jython etc.) just using the word ‘Python’ could be quite ambiguous. However, I’d like to state clearly from the outset of this article, that the ‘Python’ used in this post refers to the Cpython implementation.
Python is a High Level Programming Language.
What programmers do primarily, is write instructions for the computer with programming languages. These written instructions are usually loaded into memory and processed by the computer’s brain formally called its CPU. The exact moment when an/a instruction/program is loaded into memory and executed by the CPU will be called run-time in this article. It is important to note here that the CPU only understands 0’s and 1’s(machine code). While in some programming languages the programmer writes instructions in machine code, most programming languages today, allow the programmer to write instructions in languages that are syntactically similar to our natural languages. Languages in the former category are called low level languages, and those in the later category are called high level languages. High level languages are highly abstracted from machine code. Think of abstraction in this context as the amount of work the computer has to do to process the instructions. Because Python is a high level language that’s why we use words like: while, for, if, else and when writing our programs.
Since the CPU Could Only Process Machine Code, How Does it Process Our High Level Python Code ?
Yes! It’s how a source code written in a high-level language is converted to machine code. This is achieved by using a piece of software that takes as input a high level source code and outputs a machine level code that is then executed by the CPU. Translators either compile, interpret or assemble a high level source code into machine code. In Python, even though there is a compilation step, Python source code, for the most part, is usually interpreted into machine code. Unlike in compilation where a language’s compiler translates the entire source code before run-time, the Python interpreter translates a python source code at run-time. It does this by reading the instruction one line at a time from memory and producing its machine code equivalent that is then executed by the CPU. I mentioned earlier on that for easy access, at run-time instructions are loaded into memory. Let’s explore how this is done in Python in great detail.
Memory Allocation and Management in Python
At the heart of every programming language is memory allocation and management. In Python everything is considered an object. When we declare variables like:
At run-time, objects are created in memory. In fact we could even have objects that store other objects. For example lists, dictionaries etc. But hold on, this could actually be slightly confusing. When we declare a variable that points to a value say y=3 in Python which is considered the object? The variable on the left or the value on the right?
Well, in Python we have object references and the objects themselves. In python and most programming languages, variables are just object references. The values they point to are the objects. So going with our previous example, where y=3. y is the object reference and 3 is the object.
An object reference points to the memory address where the object it points to is stored. Every computer memory has a section called the stack and a second section called the heap. In python, object references are stored on the stack and the real objects they point to are stored on the heap. Going with our previous example when you declare a variable like so y=3, y is stored on the stack and 3 is created and stored on the heap. Furthermore, unlike in languages like Java, Python does not allow the creation of duplicate objects on the heap. For example, if you already have y=3 and then a second variable x=3 is also declared, python does not create a second object with value 3 on the heap. It just creates a new reference in the stack that points to the object with value 3 already stored in memory.
This process of allocating space to objects on the heap and object references on the stack is what’s is termed memory management. But memory management does not end there. It also encompasses removing an object from memory when it’s no longer needed. While in some languages like Rust the programmer has to manually allocate memory and deallocate memory space afterwards, in Python, memory management is done automatically under the hood through a technique called garbage collection.
Garbage Collection ?
Generally, languages that use garbage collection to deallocate space used by unused objects do this through a mechanism called reference counting. The number of times an object is referenced is kept track of. Once the number reaches 0, the object is deleted from memory. In Python, the main garbage collection technique is the reference counting too. So during a program execution in python all the objects used in the program are loaded into memory. These objects are then assigned three properties: a type, a reference count and a value.
When an object is referenced, its reference count is incremented and decremented when an object is dereferenced. If an object’s reference count is 0, the memory space allocated to that object is deallocated. An object is usually referenced when:
- It is assigned to a variable
- When it is added to a data structure or added as a property of a class
- When it is passed as an argument to a function
- Even though problematic, when an object is assigned to itself, its reference count also increases. This type of problem is called reference cycle in Python.
To solve this problem of cyclical reference that comes with reference counting garbage collection mechanism in Python, the generational garbage collection was created.
Here, all the Python objects in a given program are grouped into three conceptual layers called generations. There are: generations 0, 1 and 2. each layer has an upper bound of the number of objects it could accommodate. This upper bound is formally called its threshold. Once a generation reaches it’s threshold, the generational garbage collection algorithm executes on all the objects in that generation. The algorithm basically finds and obliterates objects with cyclical reference. All the objects that survive are then moved up to the next generation.
Basically, Python uses two techniques to free up space in the memory:
- Reference counting where an object is immediately removed from memory once its reference count reaches 0.
- Generational garbage collection, where objects with cyclical references are tracked and removed from memory.
Type System in Python
As stated earlier on, when an object is created on the heap, it is assigned three properties namely: a value, a reference count and a type(int, string, float, etc). Every programming language has a type system: a language’s set of rules that dictates how the data types of objects are declared, how and when the data type of an object is discerned among others.
A programming language is:
- Either dynamically typed or statically typed and
- Either strongly or weakly typed
Python is a Dynamically and Strongly Typed
In dynamically typed languages, an object’s type is only checked at run time. Dynamically typed languages do not require the programmer to specify the type of a construct in his/her code. This is because the construct is assigned a type on the fly based on its value. Statically typed languages on the other hand type check before a program’s execution; usually at compile time. Statically typed languages require the programmer to declare a construct’s data type before hand in his/her code.
Conclusively, Python is a high level programming language that uses an interpreter to translate code. Furthermore, Python is dynamically and strongly typed. Reference counting and generational garbage collection mechanisms are used to manage memory in Python.