The runtime you are targeting
What Python does with your source between typing it and running it, which version to write for in 2026, and the two facts about the interpreter, the GIL and the free-threaded build, that decide how the rest of this course reads.
On this page
What you will be able to do
- Describe what CPython does between source text and a running frame
- Choose a target version and a floor version for a new project in 2026
- State what the GIL does and does not prevent
- Explain what the free-threaded build changes and what it does not
Your source is compiled. It is just not compiled by you, and not before you ask for it. python3 run.py reads the file, compiles it to bytecode, and hands that bytecode to an interpreter loop that
begins executing straight away. There is no tsc step, no javac step, no linker, and no artefact
you ship in place of the source.
That compile pass is real, and it is narrow: it catches syntax and nothing else. A misspelled name, a call with the wrong number of arguments, a method the object does not have: every one compiles cleanly and fails the first time control reaches it.
# shipping.py
def total(prices):
if not prices:
raise ValueErorr("no prices")
return sum(prices)
$ python3 -c "from shipping import total; print(total([10, 20]))"
30
Where the bytecode goes
The bytecode for an imported module is cached beside the source. Import shipping once and a
__pycache__ directory appears next to it holding shipping.cpython-314.pyc. The name carries the
interpreter and its version, because the format is an implementation detail that changes between
releases. A script you name on the command line is not cached, only modules that get imported, which
is why a one-file script leaves no trace and a package leaves a __pycache__ in every directory.
You can see the instructions themselves with dis, worth knowing about even if you open it twice a
year.
import dis
def total(prices):
return sum(prices)
dis.dis(total)
3 RESUME 0
4 LOAD_GLOBAL 1 (sum + NULL)
LOAD_FAST_BORROW 0 (prices)
CALL 1
RETURN_VALUE
Instruction names are not a stable interface. They are renamed, split and added between versions, so
treat dis as a window onto what your interpreter does today rather than as documentation.
The work a compiler does for you elsewhere has to come from somewhere else here. Annotations exist and are genuinely useful, but the interpreter does not act on them; a separate tool does, which is annotations and the type checker. Everything else is tests. Neither is optional at any size, and both are cheap to set up: that is the next lesson.
Which version, and which floor
Checked on 13 August 2026. CPython 3.14 is current, at 3.14.7, released on 5 August 2026.
| Version | Status | End of life |
|---|---|---|
| 3.14 | bugfix, current release | October 2030 |
| 3.13 | bugfix | October 2029 |
| 3.12 | security fixes only | October 2028 |
| 3.11 | security fixes only | October 2027 |
| 3.10 | security fixes only | October 2026 |
Read those two columns separately: the bugfix phase closes years before the end-of-life date, and the rest of the window is security patches only. The release cycle page in the Python developer's guide is the list that stays current.
This course targets 3.14, and every sample in it runs on 3.14 as written. Where you have code that must also run on an older interpreter, take 3.12 as the floor.
- floor version
The oldest interpreter your code is required to run on, which is a separate decision from the version you develop against. It is what constrains which syntax you may write, and it belongs in your project's metadata as an explicit lower bound rather than living in somebody's head.
Two things push that floor up, one step each. TaskGroup, asyncio.timeout and ExceptionGroup all
arrived in 3.11, and this course leans on them hard once it reaches concurrency, which rules out
everything below that. The generic syntax from Python Enhancement Proposal (PEP) 695, def first[T](xs: list[T]) -> T and
type Vector = list[float], only parses from 3.12, which takes the last step. Below that the syntax
is not a warning or a degraded mode: it is a SyntaxError at import, and no amount of runtime
checking saves you from it.
Whatever floor you land on, write it as a lower bound rather than as the set of versions you happen to support this quarter. Python 3.10 reaches end of life in October 2026, so any list you write today is wrong within months, whereas a lower bound stays true until you move it deliberately.
The other thing you meet on day one is the interactive prompt. From 3.14 it highlights Python syntax
by default; set PYTHON_BASIC_REPL, or any variable that disables colour, if your terminal disagrees
with the result.
The GIL, in the quantity you need today
- GIL
A single lock inside CPython, held by whichever thread is currently running Python bytecode, so only one thread runs bytecode at a time. It does not stop threads overlapping blocking IO, and it does not stop parallelism inside a C extension that releases the lock while it works.
That is the whole of what you need before module 4. Spreading pure Python computation across threads buys you nothing, while threads remain exactly the right tool for work that spends its time waiting on sockets, disks or subprocesses.
One line tells you which build you are running, worth checking before you argue with a benchmark.
$ python3 -c "import sys; print(sys._is_gil_enabled())"
True
python3 -VV gives you the version with the compiler and build details behind it, and exactly what it
prints depends on who built your interpreter. sys._is_gil_enabled() answers the question that
matters, on either 3.14 build. A free-threaded build, with the Global Interpreter Lock (GIL) compiled out, has been officially
supported since 3.14 under PEP 779 rather than experimental, and it remains a separate optional build
with no date set for becoming the default.
Everything else about it belongs to threads, processes and the GIL: how to obtain such an interpreter, what it costs on single-threaded work and how to measure that on your own hardware, what an incompatible C extension does at import, and where subinterpreters fit. That lesson hands you a measurement rather than a number.
If you came from Java, the closest analogue to the GIL is nothing at all, and that is exactly why it surprises people: the JVM has run threads on separate cores since before most of us started. If you came from TypeScript, one thread at a time will feel familiar from the event loop, but the resemblance stops there: Python threads are real operating system threads that preempt each other, so every locking problem you have ever had still applies.
Check your understanding
Sign in to take this check
4 questions on this lesson, one at a time, with the reasoning for every option as soon as you answer. Each answer is marked on the server and stored against your account.
An account is free. There is no paid plan, no tier and nothing to buy.