Introduction
Before the interpreter takes over, Python performs three other steps: lexing, parsing, and compiling. Together, these steps transform the source code from lines of text into byte code containing instructions that the interpreter can understand. The interpreter's job is to take these code objects and follow the instructions.

Why?
So the error is produced by Python when it is translating the source code into byte code. During this step, scope is also decided. This is because the byte code will need to reference the correct variable locations. However, in this case, it is now wired to reference a variable that is not defined in the scope, causing the error at runtime.
Examples
In this snippet of code there are no syntax errors, no scope errors.
c = 10
def myfunc():
print(c)
myfunc()
In this program, the c = 1
implicitly makes the variable local. However, this is defined after the print(c)
which is not yet defined in its scope.
c = 10
def myfunc():
print(c)
c = 1
myfunc()
So if you did this:
c = 10
def myfunc():
c = 1
print(c)
myfunc()
or
c = 10
def myfunc():
global c # Changes the scope of the variable to make it accessible to the function
print(c)
c = 1
myfunc()
the code would work just fine.
Conclusion
To answer your question:
Does it go through all the code, parse it before executing them line
by line?
Yes, it does.