Lesson 0.2.1 · 35 min
Installing Python with uv: versions and support cycle
Python is not just one thing: there is a new version every October and each one lives for five years. This lesson installs the course version with uv, explains what it means for a version to be "supported" and shows how to check, from the code itself, which interpreter it is running on.
- Module
- 0.2 · Modern working environment
- Duration
- 35 min
- Author
- Jesús Cuesta
- Updated
Level 0 lessons
0.1 · Welcome and expectations
0.2 · Modern working environment
- 0.2.1 Installing Python with uv: versions and support cycle
- 0.2.2 Virtual environments and dependencies: uv init, uv add, pyproject.toml and lockfiles
- 0.2.3 Editors (VS Code, Cursor, PyCharm) and notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Basic terminal, paths, environment variables and .env files
- 0.2.5 Git and GitHub: commits, branches, pull requests and .gitignore
- 0.2.6 Introduction to AI coding assistants: prompts, review and tests
0.3 · Language fundamentals
- 0.3.1 Variables, types and operators; floating point and decimal for prices and ticks
- 0.3.2 Strings, f-strings and formatting numbers and dates
- 0.3.3 Lists, tuples, dictionaries, sets and comprehensions
- 0.3.4 Control flow: if, elif, else, for, while and match-case
- 0.3.5 Functions: arguments, *args and **kwargs, lambdas and scope
- 0.3.6 Exceptions; modules and packages, with Python 3.15 lazy imports
0.4 · Intermediate Python
- 0.4.1 Object-oriented programming: classes, inheritance, composition and dataclasses
- 0.4.2 Typing: type hints, mypy or ty, and Pydantic for configurations
- 0.4.3 Iterators, generators, decorators and context managers
- 0.4.4 Files (CSV, JSON, YAML, TOML) and dates with time zones
- 0.4.5 Professional logging and testing with pytest
- 0.4.6 Concurrency: threading, multiprocessing, asyncio and free-threading
0.5 · Numerical computing and data
- 0.5.1 NumPy 2: arrays, broadcasting, vectorization, linear algebra and random generators
- 0.5.2 pandas 3.0: Series, DataFrame, time indexes, Copy-on-Write, the str type and pd.col()
- 0.5.3 Selection, filtering, groupby, merge, pivot, resample and moving windows
- 0.5.4 Polars: lazy versus eager API, expressions, performance and when to pick it over pandas
- 0.5.5 DuckDB and essential SQL: SELECT, JOIN, GROUP BY and window functions on Parquet
0.6 · Visualisation
0.7 · Minimum mathematics and statistics
Why the version matters
A Python program does not run on its own: it is run by an interpreter, the python program that reads your code
and turns it into operations. Each version of the interpreter adds features to the language, changes details of the
standard library and, over time, stops receiving fixes. The course libraries (NumPy, pandas, Polars…) publish packages
for specific versions and drop the old ones.
For systematic trading, this has three practical consequences:
- Reproducibility. A backtest must give the same result today and a year from now. If the interpreter or a library changes, a calculation can change with them: a different default value, a rounding, a warning that did not appear before.
- Security. A live system connects to a broker with API keys. If its interpreter no longer receives security patches, any flaw that is discovered will stay open forever.
- Compatibility. What is new in libraries reaches recent versions first. Staying on an old version ends up leaving you out of the improvements in pandas 3.0 or NumPy 2 (module 0.5).
The course uses Python 3.14 (specifically 3.14.8, the version all the code you will see has been run with). 3.13 also works. This lesson explains why and how to install it.
The life cycle of a version
Since 2019, Python has released a new version every year, in October (PEP 602). Each version goes through five phases, which the Python developer's guide describes as follows1Note 1 · Python Developer's Guide, "Status of Python versions" (devguide.python.org/versions), accessed on October 21, 2026. The end-of-support dates from 3.11 on are planned and are adjusted by each version's release managers.:
Mermaid source
stateDiagram-v2 [*] --> feature feature --> prerelease: first beta prerelease --> bugfix: final release bugfix --> security: after two years security --> eol: after five years eol --> [*]
- feature and prerelease: the version is under development. Betas and release candidates are for testing libraries, not for working.
- bugfix: the version is now stable. It receives bug and security fixes, with new installers roughly every two months (3.14.1, 3.14.2…). It lasts about two years starting with 3.13; in earlier versions, 18 months.
- security: security patches only, released as source code and without installers.
- end-of-life (eol): five years after the release, support ends. The interpreter keeps working, but nobody fixes it.
The figure at the top draws that cycle for versions 3.10 to 3.15, with a line at October 2026. The thick bar is the bugfix phase; the thin one, the security-only phase. The dates come from the developer's guide:
| Version | Released | Phase in October 2026 | End of support |
|---|---|---|---|
| 3.10 | Oct 4, 2021 | End of life | Oct 1, 2026 |
| 3.11 | Oct 24, 2022 | Security only | Oct 2027 (planned) |
| 3.12 | Oct 2, 2023 | Security only | Oct 2028 (planned) |
| 3.13 | Oct 7, 2024 | Security only | Oct 2029 (planned) |
| 3.14 | Oct 7, 2025 | Bugfix | Oct 2030 (planned) |
| 3.15 | Oct 9, 2026 | Bugfix | Oct 2031 (planned) |
Two dates this month change the map. On October 1, 2026, Python 3.10 reached the end of its support: if a server or an old tutorial uses it, it is time to migrate. On October 9, Python 3.15.0 was released2Note 2 · Python 3.15.0 release notes, python.org, October 9, 2026., which among other changes uses UTF-8 as the default encoding and introduces explicit lazy imports (lesson 0.3.6).
Installing uv and Python
uv is a Python project and package manager written in Rust by Astral. It replaces several classic tools (pip, venv, pyenv, pip-tools) with a single, much faster one. In this course it does two things: install Python versions (this lesson) and manage each project's environments and dependencies (lesson 0.2.2).
It is installed with an official script. On Linux and macOS, from the terminal (the terminal is covered in more detail in lesson 0.2.4; for now, copying and pasting is enough):
curl -LsSf https://astral.sh/uv/install.sh | shOn Windows, from PowerShell:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"If you prefer a system package manager, brew install uv (macOS) or winget install --id=astral-sh.uv -e (Windows)
also work. Close and reopen the terminal and check that it responds:
uv --versionWith uv installed, installing Python is a single command:
uv python install 3.14 # the latest 3.14.x
uv python list # installed and available versions
python3.14 --version # uv leaves an executable named after the version
uv python upgrade 3.14 # move to the latest 3.14.x later onuv downloads a precompiled Python distribution and stores it in its own folder. It does not overwrite anything:
- Linux: the distribution ships its own Python, used by system tools. Do not upgrade it or install packages into it
with
sudo pip; a change there can break the system package manager. - macOS: there may be a
python3from the Xcode tools, usually old. Leave it alone. - Windows: you do not need the python.org installer or the Microsoft Store if you use uv.
Nor do you need to install Python by hand before starting a project: if a project asks for 3.14 and you do not have
it, uv downloads it when you run the first command. In practice you will almost never call python directly, but
uv run python inside a project, which picks the right version for you (lesson 0.2.2).
To pin the version of a folder, uv python pin 3.14 creates a .python-version file in it with the number. uv reads
it every time you work in that folder, and other tools understand it too. The course project will have it from the
start (lesson 0.2.2).
Checking the interpreter from Python
Installing is half the job. The other half is knowing, from the code itself, which interpreter it is running on: on
your laptop, on a server or on someone else's computer. The standard library's sys module tells you:
import sys
sys.version_infosys.version_info(major=3, minor=14, micro=8, releaselevel='final', serial=0)sys.version_info is a named tuple: major, minor and patch (micro) version, and the release level (final in a
stable release; alpha, beta or candidate in a pre-release). Since it is a tuple, it can be compared with another
tuple:
print(sys.version_info[:2])
print(sys.version_info >= (3, 13))
print(sys.version_info >= (3, 15))(3, 14)
True
FalseThe platform module adds the rest: the version as text, the implementation (CPython is the reference one, the one uv
installs) and the machine's operating system and architecture. On Linux, platform.system() returns Linux; on macOS,
Darwin; on Windows, Windows. The output below is from the machine where the course was run:
import platform
print("Python:", platform.python_version())
print("Implementation:", platform.python_implementation())
print("System:", platform.system(), platform.machine())Python: 3.14.8
Implementation: CPython
System: Linux x86_64Versions as tuples, not as text
A classic mistake when working with versions is comparing them as text strings. Python compares strings character by character, and the character "1" comes before "9":
versiones = ["3.9", "3.10", "3.14", "3.13"]
print(sorted(versiones))
print("3.9" > "3.10")['3.10', '3.13', '3.14', '3.9']
TrueGoing by the text, 3.9 is newer than 3.10. It is the same problem as sorting dates written as "9/3/2025" or amounts stored as text: the order looks reasonable until it stops being so. The solution is to convert each version into a tuple of integers before comparing it:
def como_tupla(version: str) -> tuple[int, ...]:
"""'3.10' -> (3, 10)"""
return tuple(int(parte) for parte in version.split("."))
print(sorted(versiones, key=como_tupla))
print(como_tupla("3.9") > como_tupla("3.10"))
print(max(versiones, key=como_tupla))['3.9', '3.10', '3.13', '3.14']
False
3.14Tuples are compared element by element: first the 3 with the 3 and, since they tie, the 9 with the 10, this time as
numbers. That is why sys.version_info is a tuple and not text. The packaging library, which pip and uv use under
the hood, does the same with more complex versions ("2.5.4", "3.0.0rc1"), but to compare Python versions the tuple is
enough.
Which versions are supported today
With the table above you can compute how many months of support each version has left on a given date. A fixed date (October 21, 2026) is used so that the result does not change depending on the day you run the cell; the developer's guide gives the end-of-support dates as year and month:
fin_soporte = {
"3.10": (2026, 10), "3.11": (2027, 10), "3.12": (2028, 10),
"3.13": (2029, 10), "3.14": (2030, 10), "3.15": (2031, 10),
}
hoy = (2026, 10)
for version, (anio, mes) in fin_soporte.items():
meses = (anio - hoy[0]) * 12 + (mes - hoy[1])
estado = "out of support" if meses <= 0 else f"{meses} months"
print(f"Python {version:>5}: {estado}")Python 3.10: out of support
Python 3.11: 12 months
Python 3.12: 24 months
Python 3.13: 36 months
Python 3.14: 48 months
Python 3.15: 60 monthsFor a new project, the rule of thumb: the most recent version with at least a few months of track record and every library available. For a live system, also a version with years of support left, because migrating a system that trades costs more than migrating a notebook.
The version, alongside the results
A backtest produces numbers: return, volatility, drawdown. Those numbers depend on the data, the code and the
environment. Git will store the code (lesson 0.2.5) and your datos/ folder the data; the environment has to be
recorded. A dictionary with the versions, stored alongside each result, is enough to start with:
import json
import numpy as np
import pandas as pd
entorno = {
"python": platform.python_version(),
"implementacion": platform.python_implementation(),
"sistema": platform.system(),
"numpy": np.__version__,
"pandas": pd.__version__,
}
print(json.dumps(entorno, indent=2)){
"python": "3.14.8",
"implementacion": "CPython",
"sistema": "Linux",
"numpy": "2.5.4",
"pandas": "3.0.6"
}If six months from now the same backtest gives a different Sharpe, the first step is to compare this block with the
current one. Lesson 0.2.2 goes one step further: it pins the exact versions of every library in a file (uv.lock) so
that the environment is identical on any machine.
Exercises
- Install uv and Python 3.14 on your computer. Run
uv python listand note which Python versions you already had installed (the system ones) and where they are.
Solution
The output depends on your machine. On Linux the system Python usually appears at /usr/bin/python3 (whatever version
your distribution ships) and uv's 3.14 in a folder under your user. On macOS the python3 from the Xcode tools may
appear and, if you use Homebrew, its own. On Windows, only the ones you installed yourself. What matters: the system
Python is still there and you have not touched it.
- Write a function
exigir_python(minima)that takes a tuple such as(3, 13)and raises aRuntimeErrorwith a clear message if the interpreter is older. Where would you call it in a system that trades live?
Solution
def exigir_python(minima: tuple[int, int]) -> None:
actual = sys.version_info[:2]
if actual < minima:
raise RuntimeError(
f"Python {minima[0]}.{minima[1]} or later is required; "
f"this one is {actual[0]}.{actual[1]}"
)
exigir_python((3, 13))
print("Version OK")Version OKAt the start of the program, before connecting to the broker or loading data: it is better for the system not to start than to start on an interpreter nobody has tested it with. Exceptions are covered in lesson 0.3.6.
- Extend the environment metadata cell with the execution date and time in UTC and save it to an
entorno.jsonfile. Why UTC and not local time?
Solution
from datetime import datetime, timezone
from pathlib import Path
entorno["ejecutado_utc"] = datetime(2026, 10, 21, 16, 30,
tzinfo=timezone.utc).isoformat()
Path("entorno.json").write_text(json.dumps(entorno, indent=2))
print(json.loads(Path("entorno.json").read_text())["ejecutado_utc"])2026-10-21T16:30:00+00:00In your own code you would use datetime.now(timezone.utc); here the date is fixed so that the output is always the
same. UTC because it is the same on every machine and has no daylight saving changes: a server in Frankfurt, another
in Virginia and your laptop write the same time for the same instant. Market time zones are covered in lesson 0.4.4.
Footnotes
Each answer is folded under its question
Self-assessment
A server running your live system has Python 3.10. What has changed since October 1, 2026?
- ANothing: the code keeps working and will keep receiving patches
- BPython 3.10 has stopped receiving fixes, including security fixes
- CPython 3.10 stops working on Linux
- Dpip can no longer install packages on 3.10
Show answer
Answer B. Python 3.10 has stopped receiving fixes, including security fixes
End of support does not switch anything off: the interpreter keeps working. What ends are the patches, security ones included, and little by little libraries stop publishing releases for it. A live system should not depend on an unsupported version.
What does
sorted(["3.9", "3.10", "3.14"])return?- A
['3.9', '3.10', '3.14'] - B
['3.14', '3.10', '3.9'] - CAn error, because they are not numbers
- D
['3.10', '3.14', '3.9']
Show answer
Answer D.
['3.10', '3.14', '3.9']Strings are compared character by character: "1" comes before "9", so "3.10" and "3.14" come before "3.9". Versions are compared as tuples of integers, like
sys.version_info.- A
What phase is Python 3.14 in, in October 2026?
- ABugfix: it receives bug and security fixes, with installers roughly every two months
- BDevelopment (feature)
- CSecurity only
- DEnd of life
Show answer
Answer A. Bugfix: it receives bug and security fixes, with installers roughly every two months
It was released on October 7, 2025, and versions since 3.13 get about two years of bug fixes. It will move to security-only around October 2027 and its support ends in October 2030.
Why is it worth storing the versions of Python, NumPy and pandas alongside a backtest's results?
- ABecause regulations require it
- BTo make the backtest run faster
- CTo be able to reproduce the result: a version change can change a calculation, a default value or performance
- DIt is useless if the code is in Git
Show answer
Answer C. To be able to reproduce the result: a version change can change a calculation, a default value or performance
Git stores the code, not the interpreter or the libraries. If a result changes when you repeat it months later, the first question is whether the environment changed. Lesson 0.2.2 solves the problem at the root with a lockfile.
Key points
- A new Python version comes out every October. It gets about two years of bug fixes, then security patches only, and its support ends after five years.
- In October 2026: 3.10 out of support since the 1st, 3.15 released on the 9th; the course uses 3.14 and 3.13 also works.
- uv installs and manages Python versions without touching the system Python:
uv python install 3.14. sys.version_infois a tuple: compare it with(3, 13), never compare versions as text.- A backtest result without the environment's version is not reproducible: store it alongside the numbers.