The answer depends on which interpreter you use.
At the highest level, the process will always be something like this:
Python source code → byte code → machine code → CPU runs machine code → microcode → hardware flips bits in memory
but the fine details are very complicated. Even in that simple version, I have skipped a lot of steps. (E.g. the Python source code has to be read, then parsed into a parse tree, then the parse tree has to be converted to an abstract syntax tree, which can be compiled into byte code.)
CPython (the interpreter you are probably using):
- compiles Python source code to the CPython virtual machine byte code;
- compiles Python source code to Java Virtual Machine byte code;
- compiles Python source code to dot-Net CLR (Common Language Runtime) byte code;
- runs a “Just In Time” compiler, which in simple language means that parts of the code which are reused often will be automatically re-compiled to machine code when needed.
- Like Jython, but uses the Java “Graal” virtual machine instead of the JVM.
An example may help make things clear. The Python source code:
message = "Hello, world!"
gets compiled into byte code like this in CPython 3.10:
Of course nobody can make sense of that. Its a stream of bytes! (Hence the name “byte-code”.) Fortunately CPython comes with a disassembler that turns the unreadable byte-code into symbolic form that we can read:
2 0 LOAD_CONST 0 ('Hello, World!')
2 STORE_NAME 0 (message)
3 4 LOAD_NAME 1 (print)
6 LOAD_NAME 0 (message)
8 CALL_FUNCTION 1
12 LOAD_CONST 1 (None)
Next, the interpreter runs the byte-code. Each byte-code instruction (like LOAD_CONST and CALL_FUNCTION) corresponds to machine code built into the interpreter. In the case of CPython, that machine code was built by the C compiler that make the interpreter. In the case of Jython, it was made by the Java compiler; and in the case of IronPython, it was made by one of the CLR languages, like C# or F.
However it was made, the interpreter has something that knows how to call a Python function. That CALL_FUNCTION byte-code causes the interpreter to grab the function (here,
print) and its arguments, and run the function.
CALL_FUNCTION needs to be clever enough to understand functions created with Python itself
def function... as well as built-in functions that are written in C (or Java, or C#, or F, or whatever language).
So once that happens, the next step is that the function’s code gets run by the CPU. The CPU sees the function, which is a series of machine code instructions, and runs them.
Back in the 1960s and 70s, CPUs were pretty simple, and each machine instruction corresponded to a physical circuit in the CPU that flipped bits. But in the 21st century, CPUs are much more complicated, and each machine code instruction corresponds to one or more microcode instructions, which in turn correspond to actual hardware that flips bits.
Trying to understand Python code at the level of flipping bits is almost impossible! That simple, two line “Hello World” program above would probably be tens of thousands of machine code instructions.