Okay after reading answers from Doug and Jim, I think I have an idea on how this works.
First of all of the examples work in REPL (Ipython, default)
Files:
If you write this in a file:
if True:
print("Hi")
else:
I am an error. What can you do about it?
And run the file, it will throw a SyntaxError. This proves that whenever we execute a python code from a file, it generates a bytecode and since the statement in else is not a valid python expression, we get a SyntaxError.
REPL:
With REPL things get a bit dependent. In the python interpreter if you type
>>>def foo():
if True:
print("Hey")
else:
I am an error. What can you do about it?
>>>foo()
Hey
A successful execution means no byte code right? Hold on.
If you write this:
>>>x = 10
>>>def foo():
print(x)
x += 1
>>>foo()
And Boom! everything falls apart, You get UnboundLocalError at print(x) statement. Which means bytecode is there.
So what exactly is happening here?
If python finds one single occurrence of a variable, it tries to optimize its working by reading all of them first. So, in the second example when the code encounters print(x), it tries to lookup all the operations on x. Soon it finds the statement x+=1. Since there is no mention of x in the local scope and python never looks for the variable in global scope if not mentioned explicitly, we have
UnboundLocalError: local variable 'x' is referenced before assignment
Conclusive Proof
If we write something like this:
>>>x = 10
>>>def foo():
if True:
print(x)
else:
x+=1
>>>foo()
UnboundLocalError: local variable 'x' referenced before assignment
That's it!
x+=1 was never going to be executed but since the print statement prints x and another reference(x+=1) was the issue, there was an error encountered before printing the value. The first case worked fine without the SyntaxError in REPL because it never bothered looking inside the else statement because it never mattered.