Most people, when they’re introduced to Python, are told it’s run top to bottom.

That is to say, if you write

def f():
    print("hello")
    print("world")

f()

it’ll execute the top statement first, and the second statement second.

hello
world

This helps us as teachers to explain to new programmers how to read code, and it’s a contrast to languages like Javascript.

How Javascript Does It

Javascript is asynchronous by default and so you can’t depend on something that is invoked above happening before something that is invoked below.

const first = () => {
    console.log("first!");
}
const second = () => {
    console.log("second!");
}
const third = () => {
    console.log("third!");
}
first();
setTimeout(second);
third();

Because we technically schedule the execution of second with setTimeout, even though we tell it to happen as soon as it can, we get the following

first!
third!
second!

node has moved on.

For more on this behavior, I recommend this video on Javascript’s execution model and the event loop.

The “equivalent” python code would look something like this

import time

def first():
    print("first!")

def second():
    print("second!")

def first():
    print("third!")

first()
time.sleep(0)
second()
third()

but this isn’t really the same as saying “enqueue an invocation of second.” What it’s really saying is “pause momentarily before executing second and then proceed, which it does

first!
second!
third!

without you even knowing the sleep call was there.

Without involving an async framework like asyncio we can’t really replicate the same behavior in Python, which only reinforces what we were told in the beginning: Python executes top to bottom.

You Were (sort of) Lied To

Recently, though, I was introduced to a youtube channel mCoding and he put up a video Variable Lookup Weirdness in Python that broke my mental model of Python’s execution.

This gets back to my import vs runtime post, and only reinforces my point that there is a difference, even if it’s not as clearly cut as in a compiled language.

In his video, mCoding distinguishes between the following two functions

def f():
    print(x)
def g():
    print(x)
    x = 1

My inclination when watching this was that both functions would have the same result, but they don’t.

>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
NameError: name 'x' is not defined
>>> g()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in g
UnboundLocalError: local variable 'x' referenced before assignment

This is only possible because Python doesn’t just evaluate code top to bottom on the fly. Code is executed top to bottom (as long as it’s spaghetti code), but it’s interpreted first and that interpretation allows for what looks, to us at runtime, like look-ahead.

This is to say nothing of a language like Rust which can tell that this type of thing will happen at compile time, instead of letting us define the function and only complaining when it’s executed, but that’s a story for another time.