Modern Python Basics #1: Getting Started and uv Setup

6 min read

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’s switch
  • Built-in generics (3.9) — you can write list[int], dict[str, int] directly
  • Union shorthand (3.10) — int | None instead of Optional[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 + virtualenv bundled into a single binary, made 10–100x faster.

Why it’s becoming the standard, briefly:

Existing toolsuv
SpeedTens of seconds to minutesHundreds of ms
Python interpreter installpyenv separatelyOne line: uv python install
Project setuppython -m venv + source .venv/bin/activate + pip installuv init + uv add
Lock filerequirements.txt by hand / poetry.lockuv.lock automatic
Standard complianceVaries per toolPEP 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:

install uv
curl -LsSf https://astral.sh/uv/install.sh | sh

If you use Homebrew:

install via brew
brew install uv

Windows (PowerShell):

windows install
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

After installation, open a new terminal and check the version.

check version
uv --version
# uv 0.5.x  (or later)

Installing Python 3.14 #

uv installs Python interpreters directly. You don’t need pyenv separately.

install Python
uv python install 3.14

To list installed interpreters:

list installed Pythons
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.

initialize project
mkdir hello-py
cd hello-py
uv init --python 3.14

The --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:

generated 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:

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.

first run
uv run main.py
# Hello from hello-py!

What uv run does:

  1. Finds an interpreter that satisfies the requires-python in pyproject.toml (downloads automatically if missing)
  2. Automatically creates the project’s virtual environment (.venv/) and syncs it with uv.lock
  3. 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.

add a dependency
uv add httpx

This single line does several things at once:

  • Adds httpx to dependencies in pyproject.toml
  • Records the exact version and hash in uv.lock
  • Installs it into .venv/

If you reopen pyproject.toml:

dependencies added
[project]
# ...
dependencies = [
    "httpx>=0.28.1",
]

Tools needed only at development time (test runners, linters) go into the --dev group.

dev dependencies
uv add --dev pytest ruff

These are excluded at deploy time and only installed during development. To remove a package:

remove
uv remove httpx

One more time — the whole flow #

zero-to-running in one go
# 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 pytest

Here’s what the same thing used to take:

old flow (for reference)
# 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.py

It 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.

hello.py
# /// script
# requires-python = ">=3.14"
# dependencies = ["httpx"]
# ///
import httpx

resp = httpx.get("https://httpbin.org/get")
print(resp.json())

To run:

standalone script
uv run hello.py

uv 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 run are the daily workflow
  • pyproject.toml is 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.

X