Modern Python Basics #1: Getting Started and uv Setup
This blog already has a Python Basics tutorial. It started in 2017 and the code and screenshots inside are still from the Python 3.5–3.9 era. Python has changed a lot since then. So the old tutorial stays as is, and here we put together seven new posts on how you would start Python today.
- #1 Getting started and uv setup ← this post
- #2 Variables, basic types, and type hints
- #3 Control flow — if, while, for, match-case
- #4 Collections and comprehensions
- #5 Functions — argument patterns
- #6 Errors and exception handling
- #7 Modules, packages, and pyproject.toml
Why Python again? #
The Python of the old tutorial era and the Python of today are different enough that it almost feels awkward to call them the same language. Just a few of the major changes:
- Type hints are the standard convention. Most library code carries types
match-case(3.10) — pattern matching, different in feel from JavaScript’sswitch- Built-in generics (3.9) — you can write
list[int],dict[str, int]directly - Union shorthand (3.10) —
int | Noneinstead ofOptional[int] - Exception groups (3.11) — handle simultaneous exceptions with
except* - Free-threaded (3.13–3.14, PEP 779) — GIL-less builds reach official support
- t-strings (3.14, PEP 765) — template literals with deferred interpolation, distinct from f-strings
- Lazy annotations (3.14, PEP 649) — annotations are evaluated lazily by default
The toolchain has changed almost completely too. Old flows like calling pip directly, requirements.txt, and manually activating virtualenv are barely used anymore. Instead, one tool — uv — absorbs most of that role.
What is uv? #
uv is a Rust-based Python package and project manager built by Astral (the team behind Ruff). One-line summary:
pip+pip-tools+pipx+poetry+pyenv+virtualenvbundled into a single binary, made 10–100x faster.
Why it’s becoming the standard, briefly:
| Existing tools | uv | |
|---|---|---|
| Speed | Tens of seconds to minutes | Hundreds of ms |
| Python interpreter install | pyenv separately | One line: uv python install |
| Project setup | python -m venv + source .venv/bin/activate + pip install | uv init + uv add |
| Lock file | requirements.txt by hand / poetry.lock | uv.lock automatic |
| Standard compliance | Varies per tool | PEP 621 pyproject.toml as is |
In this post we use uv as the default and handle interpreters, virtual environments, dependencies, and execution all with uv.
Install #
macOS / Linux:
curl -LsSf https://astral.sh/uv/install.sh | shIf you use Homebrew:
brew install uvWindows (PowerShell):
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"After installation, open a new terminal and check the version.
uv --version
# uv 0.5.x (or later)Installing Python 3.14 #
uv installs Python interpreters directly. You don’t need pyenv separately.
uv python install 3.14To list installed interpreters:
uv python list --installed
# cpython-3.14.x-macos-aarch64-none ...This interpreter is permanently installed on your system, but it’s not exposed directly on PATH. uv picks the right one per project automatically. It doesn’t mix with other Pythons on your system, so it’s safe.
First project #
Make an empty directory and initialize the project with uv init.
mkdir hello-py
cd hello-py
uv init --python 3.14The --python 3.14 is the key. This project is pinned to 3.14. Even if another project uses 3.12, they don’t interfere. Running the command creates these files:
hello-py/
├── .python-version # 3.14
├── pyproject.toml # project metadata + dependencies
├── README.md
└── main.py # print("Hello from hello-py!")If you open pyproject.toml:
[project]
name = "hello-py"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.14"
dependencies = []It’s the PEP 621 standard format as is. Think of this file as the single definition of the project. Dependencies, Python version, build info, and tool settings (ruff/mypy, etc.) all gather here. You’ll rarely create requirements.txt, setup.py, or setup.cfg again.
First run #
Let’s run main.py as is. But not python main.py — it’s uv run main.py.
uv run main.py
# Hello from hello-py!What uv run does:
- Finds an interpreter that satisfies the
requires-pythoninpyproject.toml(downloads automatically if missing) - Automatically creates the project’s virtual environment (
.venv/) and syncs it withuv.lock - Runs the command inside that environment
You no longer need to activate the virtual environment manually (source .venv/bin/activate). The flow shifts to prefixing every command with uv run. It takes a moment to adjust, but once you’re used to it, accidents like forgetting to activate and installing packages into the system Python disappear.
Adding dependencies #
Use uv add to add a package.
uv add httpxThis single line does several things at once:
- Adds
httpxtodependenciesinpyproject.toml - Records the exact version and hash in
uv.lock - Installs it into
.venv/
If you reopen pyproject.toml:
[project]
# ...
dependencies = [
"httpx>=0.28.1",
]Tools needed only at development time (test runners, linters) go into the --dev group.
uv add --dev pytest ruffThese are excluded at deploy time and only installed during development. To remove a package:
uv remove httpxOne more time — the whole flow #
# 1. Install the tool (once)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 2. Create the project
uv init my-app --python 3.14
cd my-app
# 3. Add dependencies
uv add httpx
uv add --dev pytest
# 4. Run
uv run main.py
uv run pytestHere’s what the same thing used to take:
# Install Python with pyenv
pyenv install 3.14.0
pyenv local 3.14.0
# Create a venv
python -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install httpx
pip install pytest
pip freeze > requirements.txt
# Run
python main.pyIt does the same thing, but without a lock file, reproducibility is poor, and pip freeze doesn’t distinguish direct vs. transitive dependencies. uv is lock-based from the start, so the exact same environment can be reproduced on another machine.
Using it at script scale — a lighter option #
When you only want to run one small script and even setting up a project feels heavy, there’s a mode where the script itself declares its dependencies.
# /// script
# requires-python = ">=3.14"
# dependencies = ["httpx"]
# ///
import httpx
resp = httpx.get("https://httpbin.org/get")
print(resp.json())To run:
uv run hello.pyuv recognizes the dependencies declared in # ///, builds a temporary environment automatically, and runs the script. Very useful for one-off scripts, automation, and small tools.
Wrap-up #
What this post covered:
- The changes Python 3.14 brought — type hints first, match-case, exception groups, free-threaded, t-strings
- One tool,
uv, to integrate interpreter install + project setup + dependencies + execution - Three commands
uv init,uv add,uv runare the daily workflow pyproject.tomlis the single definition of the project- A flow without virtual environment activation — prefix every command with
uv run
In the next post (#2 Variables, basic types, and type hints), we cover the most basic — types and type hints. Why write types in a dynamic language, modern syntax like int | None, built-in generics like list[int], all the way to mypy/pyright.