How does python know to [not execute the code immediately]?
The short answer is that Python has a compilation step, during which it scans the entirety of the function, spots it is a generator function, and so produces bytecode for a generator function that is different to the bytecode it would generate for a normal function. This means that at runtime, when a generator function is invoked, Python will know to create a generator object rather than to immediately start executing the body of the function.
The longer answer is that Python sits in a halfway house between strictly interpreted languages (eg. bash) and strictly compiled languages (eg. C). Instead, Python, like Java, has a compilation step that produces "bytecode". It is this bytecode that is interpreted by the Python virtual machine rather than the source code. Unlike Java, Python's compilation step is usually performed immediately before execution, rather than ahead of time. As such, some users may not even be aware that the compilation step exists. That is, when you do python mymodule.py, Python will both compile the module and then execute it. However, you can pre-compile Python code using the compileall module or the compile function eg.
# create the source code
echo "print('hello world')" > mymodule.py
# assert that no byte code file exists
[ -f __pycache__/mymodule.*.pyc ] || echo no byte code exists
# create the byte code file
python -m compileall mymodule.py
# check that the byte code file now exists
[ -f __pycache__/mymodule.*.pyc ] && echo byte code has been created
# directly execute the byte code rather than the source code
python __pycache__/mymodule.*.pyc
Outputs:
no byte code exists
Compiling 'mymodule.py'...
byte code has been created
hello world
Key differences between compiled and interpreted languages
In interpreted languages, each statement is evaluated one at a time. The interpreter will not consider statements after the current statement being evaluated. This means that a bit of source code could contain syntax errors, but still be partially executed by the interpreter eg.
# Bash source code file
echo start # will be executed normally
while for # syntax error
echo end # will *not* be executed because of prior syntax error
In compiled languages, a source unit must be compiled into a machine code before it can be executed. Any syntax errors will prevent compilation, and therefore nothing can be executed. eg.
// C source code file
#include <stdio.h>
// this function cannot be executed as the function that comes after it has a syntax
// error. So the source cannot be compiled successfully.
int main() {
puts("start");
puts("end");
return 0;
}
void has_syntax_error() { // syntax error, missing a closing `}`
Python exhibits both traits of an interpreted language and a compiled language. Python is like an interpreted language because its programs cannot be executed without a Python interpreter. It is like a compiled language in that the source code for a single module is considered all at once, and a single syntax error will prevent any part of the code from being compiled/executed. Also, the source code is not directly executable, but must instead be compiled to a representation that the interpreter can understand. Sometimes this step is done entirely in memory and during the same process that will execute the program.
How Python knows not to start executing code in a generator function
It is during the compilation step that Python is able to detect the function is a generator. During compilation Python will scan the entirety of a function, it will spot the yield keyword and then produces bytecode for a generator function that is different to a normal function. When this function is invoked at runtime, Python will know to create a generator object rather than executing the body of the function. The implementation details can vary between different versions of Python, but the net effect remains the same.
At runtime, you can demonstrate that Python knows ahead of function invocation which functions are normal functions and which are generator functions. This is thanks to the initial compilation step. For example:
import inspect
def function(): pass
def generator(): yield
assert not inspect.isgeneratorfunction(function)
assert inspect.isgeneratorfunction(generator)