What happens between your file and the answer
Python is not read line by line as it runs
The common picture — Python reads line 1, does it, reads line 2 — is wrong in a way that matters later.
When you run python3 budget.py, four things happen:
- The whole file is read and tokenised. This is why a missing bracket on line 40 stops line 1 from running: the file never became a program.
- It is parsed into a tree representing the structure of the code.
- It is compiled to bytecode — a compact instruction set for a made-up machine.
- A virtual machine executes that bytecode, one instruction at a time.
The bytecode is not a secret. You can look at it:
import dis
dis.dis(compile("total = a + b", "<demo>", "exec")) LOAD_NAME 0 (a)
LOAD_NAME 1 (b)
BINARY_OP 0 (+)
STORE_NAME 2 (total)Four instructions: fetch a, fetch b, add them, store the result. This is a real skill occasionally — dis settles arguments about which of two ways of writing something does less work.
The __pycache__ folder
Import a module and Python writes the compiled bytecode into a folder called __pycache__, as a .pyc file, so the next import can skip steps 1 to 3. It checks the source file's timestamp and size and recompiles when they change.
Two practical consequences. It is always safe to delete __pycache__; the worst outcome is one slightly slower start. And it belongs in .gitignore — committing it puts machine-specific compiled files in your repository for no benefit.
The file you run directly never gets cached, only the modules it imports. That is why a large program starts faster the second time and your one-file script does not.
Why Python is slow, stated properly
"Python is slow" is repeated everywhere and almost never explained. The mechanism is this: every value is a heap-allocated object carrying its type.
When C adds two integers, the compiler already knows they are integers and emits one CPU instruction. When Python evaluates a + b, the virtual machine must: look up what a currently refers to, look up its type, find that type's addition method, do the same for b, check they are compatible, allocate a new object for the result, and update reference counts. That is dozens of machine instructions for one addition, and none of it can be skipped, because a could have been an int on the last iteration and a string on this one.
Measure it:
python3 -m timeit -s "xs=range(1000000)" "sum(x*x for x in xs)"
python3 -m timeit -s "import numpy as np; xs=np.arange(1000000)" "(xs*xs).sum()"On an ordinary laptop the first is roughly 60–90 milliseconds and the second roughly 1–2 milliseconds. That is a 50-fold difference for the same arithmetic. NumPy is not cleverer Python; the loop runs inside compiled C over a block of raw 64-bit integers with no per-value type checks and no object allocation.
This is the whole reason a later module in this course teaches arrays and dataframes. You do not make a numeric Python loop fast by writing better Python. You move the loop somewhere else. Rewriting it more elegantly buys perhaps 30 per cent; moving it into NumPy buys a factor of fifty.
What this does and does not license
It does not mean Python is a bad choice. For an AI script, almost all the wall-clock time is spent waiting for a network reply or for a GPU that a C library is driving. The Python layer is co-ordinating, not computing, and its overhead disappears into the noise. That is exactly why the field standardised on it.
It does mean you should notice when your own code is doing the arithmetic in a loop over hundreds of thousands of items, because that is the one situation where the language choice shows up in the clock.
The other implementations
CPython, from python.org, is the one you have. There are others:
- PyPy (free,
pypy.org) runs a just-in-time compiler and can be several times faster on pure-Python loops, at the cost of slower startup and imperfect support for some C extensions. - CPython itself has been getting faster since 3.11 — the specialising interpreter gained roughly 25 per cent across a broad benchmark suite, and 3.13 added an experimental build without the global lock.
None of these change the picture above. They shrink the constant; the per-object cost is structural.
If you take one thing from this lesson: the difference between fast and slow numeric Python is almost never how the loop is written. It is whether the loop is in Python at all.
The one thing to keep
Python compiles the whole file to bytecode before running any of it, and its slowness comes from every value being a typed heap object, which is why moving a numeric loop into NumPy wins fifty-fold where rewriting it in Python wins thirty per cent.
Before you move on
A script spends 40 seconds looping over 2 million rows doing arithmetic. A developer rewrites the loop more elegantly with comprehensions and helper functions and gets 34 seconds. What does this outcome tell you?
Pick the one you would defend. Nobody sees your answer.