Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 1 – Getting started: install, run and read Python

uv, pipx and inline script metadata in Python

Create a Python project with uv, run a single-file script that declares its own dependencies, and install command-line tools in isolation with pipx or uv.

Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Create a project and add dependencies with uv init, uv add and uv run
  • Explain what uv.lock records that pyproject.toml does not
  • Write an inline metadata block that uv run and pipx run can read
  • Install command-line tools in isolation with pipx or uv tool
  • Choose between venv and pip, uv and pipx for a task

Before you start

What you’ll learn · Before you start

What you will learn

  • Create a project and add dependencies with uv init, uv add and uv run
  • Explain what uv.lock records that pyproject.toml does not
  • Write an inline metadata block that uv run and pipx run can read
  • Install command-line tools in isolation with pipx or uv tool
  • Choose between venv and pip, uv and pipx for a task

Before you start

On this page

A project with three packages needs four steps on every computer it runs on: make an environment, install the right versions into it, record those versions, and repeat all of it after each change. The venv and pip of the previous lesson do each step, and the steps go stale the day someone forgets one. Two tools do this bookkeeping for you: uv manages a whole project, and pipx installs command-line applications, each in an environment of its own.

Below you set up a project with uv and see what it checks before every run, compare a range of versions with a lock file, give a single script its own list of dependencies, and install tools so that they never clash with your projects. Neither tool is part of Python: both are open-source programs that you install once.

uv keeps a project’s environment in step with pyproject.toml

uv, made by Astral and licensed under MIT or Apache-2.0, is one program that does the jobs of several: it installs Python versions (uv python install 3.14), creates virtual environments, works out which versions of your dependencies fit together, records them, and runs your code. Install it with a package manager (brew install uv on macOS, winget install --id=astral-sh.uv -e on Windows), with pipx install uv, or with the installer script from uv’s documentation. A script you download and run has the same access to your computer as you, so look at it first; the documentation shows how.

Version note

This lesson follows the documentation of uv 0.13 and pipx 1.18. Both change faster than Python, so if a command behaves differently, compare uv --version or pipx --version with their documentation.

Three commands start a project:

  1. uv init weather-notes creates the folder weather-notes with a pyproject.toml, a .python-version file that names the Python version to use, a README.md, a .gitignore, a Git repository and starter code in src/weather_notes/__init__.py.
  2. uv add rich, in that folder, writes the requirement rich>=15.0.0, the newest version that fits as the lowest allowed, into pyproject.toml. It also creates .venv if there is none, installs rich into it and records the exact versions in uv.lock.
  3. uv run weather-notes runs the command that uv init set up, and uv run some_file.py runs any file, inside the project’s environment.

What makes uv run dependable is what it checks before it runs anything. Step through one run:

Interactive · stage by stage What uv run does before it runs your command Step through one uv run in the weather-notes project, from pyproject.toml to the command.
What uv run does before it runs your command, step 5 of 5. Showing uv run weather-notes, Read pyproject.toml, Is uv.lock up to date? (locked again if needed), Is .venv in step? (in step with uv.lock), Run the command. In focus: Run the command.uv run weather-notesin the project's folderRead pyproject.tomlrich>=15.0.0: a rangeIs uv.lock up to date?locked again if neededIs .venv in step?in step with uv.lockRun the commandinside .venv

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. You type uv run. In the project's folder you type uv run weather-notes. Nothing is activated, and you do not have to remember whether anyone changed the dependencies.
  2. The ranges. uv reads pyproject.toml. Its dependencies are ranges, such as rich>=15.0.0, which say which versions would be acceptable, not which one is installed.
  3. The lock. uv checks that uv.lock still matches pyproject.toml. If someone changed the dependencies since the last lock, it locks again first, so the lock always records one exact version of every package.
  4. The environment. uv checks that .venv holds what uv.lock lists, and installs anything missing or at another version. The very first run creates .venv as well.
  5. The command runs. Only now does uv run weather-notes, inside .venv. Every run passes these checks first, so nobody runs the project with a stale set of packages.

So you never activate anything and never run with a stale set of packages. uv remove rich takes a dependency out again, uv sync makes .venv match uv.lock exactly (after you pull a teammate’s changes, for example), and uv lock --upgrade-package rich moves one package to its newest allowed version.

pyproject.toml is not a uv invention: it is the standard file in which a Python project describes itself, written in Tom's Obvious, Minimal Language, a text format for settings: key = value lines grouped under headings in square brackets., and any tool can read it. The standard library reads it with tomllib:

Read what a project needs from pyproject.toml

project/pyproject.toml

[project]
name = "weather-notes"
version = "0.1.0"
description = "Short notes about the weather"
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
    "rich>=15.0.0",
]

[project.scripts]
weather-notes = "weather_notes:main"

[build-system]
requires = ["uv_build>=0.13.1,<0.14"]
build-backend = "uv_build"

project/read_project.py

import tomllib

# tomllib, in the standard library since Python 3.11, reads TOML; it needs the file opened in binary mode.
with open("pyproject.toml", "rb") as file:
    pyproject = tomllib.load(file)

project = pyproject["project"]
print("Project:", project["name"], project["version"])
print("Needs Python", project["requires-python"])
for requirement in project["dependencies"]:
    print("Depends on", requirement)
for command, target in project["scripts"].items():
    print("Adds the command", command, "which runs", target)
print("Built by", pyproject["build-system"]["build-backend"])

Output

Project: weather-notes 0.1.0
Needs Python >=3.14
Depends on rich>=15.0.0
Adds the command weather-notes which runs weather_notes:main
Built by uv_build

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 read_project.py

The [project] table holds the name, the version, the Python versions the project supports and its dependencies; [project.scripts] adds the weather-notes command, and [build-system] names uv_build, the backend that turns the project into a package. Since uv 0.12, uv init gives an application a build system by default.

Checkpoint

Check: A project managed by uv

  1. Question 1 of 2 In the weather-notes project you run uv add rich. What changes?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: pyproject.toml gets the requirement rich>=15.0.0, uv.lock records exact versions, and rich is installed into .venv

    uv add writes the requirement into pyproject.toml (the newest fitting version as the lowest allowed), installs the package into .venv and records the exact versions in uv.lock.

  2. Question 2 of 2 What does uv run check before it runs your command?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: That uv.lock matches pyproject.toml, and that .venv holds what uv.lock lists

    Before each run, uv makes sure that the lock is up to date with pyproject.toml and that the environment is up to date with the lock, then runs the command inside .venv.

A lock file turns ranges into one exact set of versions

A requirement such as rich>=15.0.0 is a version specifier: The part of a requirement that says which releases are acceptable, such as >=1.2,<2 or ~=1.2. Releases are compared number by number, and pip installs the newest one that meets every condition. More in “Finding and installing third-party packages”, a range: any version from 15.0.0 on is acceptable. Which version you actually get is decided when the environment is built, and it can change from one week to the next. This program follows a made-up package, tablekit, through its releases in weeks 1, 5 and 9, and compares a fresh install from the range with an install from the lock:

A range or a lock, in weeks 1, 5 and 9 Python · lock/range_or_lock.py
# tablekit is a made-up package: the versions its index offers in three weeks.
releases = {
    1: ["2.3.0"],
    5: ["2.3.0", "2.4.0"],
    9: ["2.3.0", "2.4.0", "3.0.0"],
}
locked = "2.3.0"  # what uv.lock recorded in week 1


def as_numbers(version):
    return tuple(int(part) for part in version.split("."))


def newest_allowed(available, minimum):
    allowed = [v for v in available if as_numbers(v) >= as_numbers(minimum)]
    return max(allowed, key=as_numbers)


print("Week  tablekit>=2.3.0 gives  uv.lock gives")
for week, available in releases.items():
    print(f"{week:>4}  {newest_allowed(available, '2.3.0'):<21}  {locked}")

Output

Week  tablekit>=2.3.0 gives  uv.lock gives
   1  2.3.0                  2.3.0
   5  2.4.0                  2.3.0
   9  3.0.0                  2.3.0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 range_or_lock.py

In week 9 a fresh install from the range gets 3.0.0, a new major version that may rename what your code calls, while everyone who installs from the lock still gets 2.3.0. A lock file: A file that records every package an application installs, direct and indirect, with its exact version, where each file came from and the file's hash, so the next install gets the same files. pylock.toml is the standard format. records the exact version of every package that was installed, including the dependencies of your dependencies: rich 15.0.0 itself needs markdown-it-py and Pygments, so uv.lock lists them too, each with its exact version. Press Edit, add "3.1.0" to week 9’s list and run it again: the range moves on, the lock does not.

uv keeps its lock in uv.lock, which belongs in version control and is never edited by hand (the .venv folder, again, is not committed). A standard format for lock files, pylock.toml, is defined by PEP 751: uv export --format pylock.toml writes one, and pip 26.2.1 has an experimental pip lock command that writes one too. The requirements.txt that pip freeze writes is the older and simpler way to pin versions, and still the most widely supported one.

Key idea

pyproject.toml says which versions would be acceptable; the lock file says which ones everyone installs, until someone upgrades it on purpose and tests the result.

Checkpoint

Check: Ranges and lock files

  1. One question A project requires tablekit>=2.3.0, and the index now offers 2.3.0, 2.4.0 and 3.0.0. Which version does a fresh install without a lock file get?

    Choose one answer.

    Show the answer: One question

    Answer: 3.0.0, the newest version that the range allows

    A range says which versions would be acceptable. Without a lock, every fresh install picks the newest acceptable version on that day, which is how week 9 of range_or_lock.py gets 3.0.0 while the lock keeps 2.3.0.

A script can carry its own dependency list

Not everything is a project. A single file that you run now and then can say what it needs in a comment block at its top, called inline script metadata: A comment block in a Python script, opened by # /// script and closed by # ///, whose lines hold TOML such as requires-python and dependencies. Script runners read it; Python itself ignores it., a packaging standard since PEP 723:

A script that names the package it needs Python · inline/report.py
# /// script
# requires-python = ">=3.14"
# dependencies = [
#   "rich",
# ]
# ///
from rich import print

print("[bold green]Report ready[/bold green]")

Output (exit status 1)

Printed as an error (standard error)

Traceback (most recent call last):
  File "report.py", line 7, in <module>
    from rich import print
ModuleNotFoundError: No module named 'rich'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 report.py

Python itself ignores the block: to python3 it is only comments. That is why the run above, with plain CPython 3.14.8, stops at the import with ModuleNotFoundError. A tool that reads the block does better: uv run report.py and pipx run report.py read it, build an environment that contains rich, and run the script there. pipx keeps that environment and builds a fresh one when the list of dependencies changes.

The rules are short. The block starts with a line that is exactly # /// script and ends with a line that is exactly # ///. Every line between them is a comment: take away the # and the space after it, and what is left is TOML, with the keys requires-python (the Python versions the script runs on) and dependencies (a list of requirements, like the one in pyproject.toml). A script may have only one script block; tools refuse a file with two. Step through a program that reads the block of report.py the way such a tool does:

Interactive · line by line Reading the metadata block of report.py Step through the loop over report.py's lines. The flag is the line the loop reads next; watch inside switch on and off.

Program · inline/read_metadata.py

import tomllib
from pathlib import Path

src = Path("report.py").read_text()
inside = False
toml = []
for line in src.splitlines():
    if line == "# /// script":
        inside = True
    elif line == "# ///":
        inside = False
    elif inside:
        toml.append(line[2:])

meta = tomllib.loads("\n".join(toml))
print("requires-python:", meta["requires-python"])
print("dependencies:", meta["dependencies"])

Variables

After line 17: Path is Path; src is '# /// script\n# requires-python = ">=3.14"\n# dependencies…; inside is False; toml is ['requires-python = ">=3.14"', 'dependencies = [', ' "rich",', ']']; line is 'print("[bold green]Report ready[/bold green]")'; meta is {'requires-python': '>=3.14', 'dependencies': ['rich']}.PathPathsrc'# /// script\n# requires-python = ">=3.14"\n# dependencies…insideFalsetoml'requires-python = ">=3.14"'0'dependencies = ['1' "rich",'2']'3line'print("[bold green]Report ready[/bold green]")'meta'requires-python':'>=3.14''dependencies':['rich']
  • just taken by the loop
  • changed in this step
  • passed by the loop
  • read next
  • where the loop stopped

Printed so far

requires-python: >=3.14
dependencies: ['rich']

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 49 steps are listed below.

Line run 17
Next line end
Loop pass –
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. import tomllib loads tomllib.
  3. Line 2. from pathlib import Path loads Path.
  4. Line 4. src = Path("report.py").read_text() sets src to '# /// script\n# requires-python = ">=3.14"\n# dependencies….
  5. Line 5. inside = False sets inside to False.
  6. Line 6. toml = [] makes toml a list of 0 items.
  7. Line 7. for line in src.splitlines(): takes the item at position 0 of src.splitlines(): line is '# /// script'.
  8. Line 8. line == "# /// script" is true (line is '# /// script'), so the block under if line == "# /// script": runs.
  9. Line 9. inside = True sets inside to True (it was False). The opening line is exactly '# /// script', so the lines after it belong to the block.
  10. Line 7. for line in src.splitlines(): takes the item at position 1 of src.splitlines(): line is '# requires-python = ">=3.14"'.
  11. Line 8. line == "# /// script" is false (line is '# requires-python = ">=3.14"'), so Python tests the next condition.
  12. Line 10. line == "# ///" is false (line is '# requires-python = ">=3.14"'), so Python tests the next condition.
  13. Line 12. inside is true (inside is True), so the block under elif inside: runs.
  14. Line 13. toml.append(line[2:]) adds 'requires-python = ">=3.14"' to the end of toml. A line inside the block loses its first two characters, the # and the space. What is left is a line of TOML.
  15. Line 7. for line in src.splitlines(): takes the item at position 2 of src.splitlines(): line is '# dependencies = ['.
  16. Line 8. line == "# /// script" is false (line is '# dependencies = ['), so Python tests the next condition.
  17. Line 10. line == "# ///" is false (line is '# dependencies = ['), so Python tests the next condition.
  18. Line 12. inside is true (inside is True), so the block under elif inside: runs.
  19. Line 13. toml.append(line[2:]) adds 'dependencies = [' to the end of toml.
  20. Line 7. for line in src.splitlines(): takes the item at position 3 of src.splitlines(): line is '# "rich",'.
  21. Line 8. line == "# /// script" is false (line is '# "rich",'), so Python tests the next condition.
  22. Line 10. line == "# ///" is false (line is '# "rich",'), so Python tests the next condition.
  23. Line 12. inside is true (inside is True), so the block under elif inside: runs.
  24. Line 13. toml.append(line[2:]) adds ' "rich",' to the end of toml.
  25. Line 7. for line in src.splitlines(): takes the item at position 4 of src.splitlines(): line is '# ]'.
  26. Line 8. line == "# /// script" is false (line is '# ]'), so Python tests the next condition.
  27. Line 10. line == "# ///" is false (line is '# ]'), so Python tests the next condition.
  28. Line 12. inside is true (inside is True), so the block under elif inside: runs.
  29. Line 13. toml.append(line[2:]) adds ']' to the end of toml.
  30. Line 7. for line in src.splitlines(): takes the item at position 5 of src.splitlines(): line is '# ///'.
  31. Line 8. line == "# /// script" is false (line is '# ///'), so Python tests the next condition.
  32. Line 10. line == "# ///" is true (line is '# ///'), so the block under elif line == "# ///": runs.
  33. Line 11. inside = False sets inside to False (it was True). Key point '# ///' closes the block. The lines after it are the program's own code, which is not part of the metadata.
  34. Line 7. for line in src.splitlines(): takes the item at position 6 of src.splitlines(): line is 'from rich import print'.
  35. Line 8. line == "# /// script" is false (line is 'from rich import print'), so Python tests the next condition.
  36. Line 10. line == "# ///" is false (line is 'from rich import print'), so Python tests the next condition.
  37. Line 12. inside is false (inside is False), so Python skips the block under elif inside:.
  38. Line 7. for line in src.splitlines(): takes the item at position 7 of src.splitlines(): line is ''.
  39. Line 8. line == "# /// script" is false (line is ''), so Python tests the next condition.
  40. Line 10. line == "# ///" is false (line is ''), so Python tests the next condition.
  41. Line 12. inside is false (inside is False), so Python skips the block under elif inside:.
  42. Line 7. for line in src.splitlines(): takes the item at position 8 of src.splitlines(): line is 'print("[bold green]Report ready[/bold green]")'.
  43. Line 8. line == "# /// script" is false (line is 'print("[bold green]Report ready[/bold green]")'), so Python tests the next condition.
  44. Line 10. line == "# ///" is false (line is 'print("[bold green]Report ready[/bold green]")'), so Python tests the next condition.
  45. Line 12. inside is false (inside is False), so Python skips the block under elif inside:.
  46. Line 7. for line in src.splitlines(): finds no item at position 9 of src.splitlines() (it has 9 items), so the loop ends.
  47. Line 15. meta = tomllib.loads("\n".join(toml)) sets meta to {'requires-python': '>=3.14', 'dependencies': ['rich']}. tomllib reads the collected lines as TOML, which gives a string for requires-python and a list for dependencies.
  48. Line 16. print("requires-python:", meta["requires-python"]) prints requires-python: >=3.14.
  49. Line 17. print("dependencies:", meta["dependencies"]) prints dependencies: ['rich']. That was the last line, so the program ends.

The loop kept the four lines between the two markers, each without its first two characters, and tomllib turned them into the string >=3.14 and the list ['rich'].

uv can also write the block for you: uv init --script report.py creates a new script with one, and uv add --script report.py rich adds a dependency to it. uv expects a dependencies key even when the list is empty, and a script is locked only when you ask, with uv lock --script report.py, which writes report.py.lock.

Checkpoint

Check: Inline script metadata

  1. One question Which line starts an inline script metadata block?

    Choose one answer.

    Show the answer: One question

    Answer: # /// script

    The block opens with a line that is exactly # /// script and closes with a line that is exactly # ///; the comment lines between them hold TOML.

pipx and uv tool give each application its own environment

Some packages are applications that you run as commands, such as the linter Ruff, rather than libraries that your code imports. Install those with pipx: A tool that installs each Python application into a virtual environment of its own and links only its commands into a folder on PATH, ~/.local/bin by default. uv tool does the same job. or with uv tool: each application gets a virtual environment of its own, so its dependencies never clash with your projects or with each other, and only its commands are put on your PATH. Step through one install:

Interactive · step by step One application, one environment of its own Step through pipx install ruff, then a run of ruff from a project folder.
One application, one environment of its own, step 5 of 5. Showing pipx install ruff, ruff's own environment, Your project's .venv, ~/.local/bin, ruff check.createslinks the commandfound on PATHpipx install rufftyped onceruff's own environmentruff and what it needsYour project's .venvyour packages~/.local/bina folder on PATHruff checktyped in any folder

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. An environment for ruff alone. pipx makes a virtual environment for ruff alone and installs ruff and everything it needs into it. Nothing goes into your project or into the system's Python.
  2. Only the command is linked. pipx puts ruff's command into a folder on PATH, ~/.local/bin on Linux; pipx ensurepath adds that folder to PATH if it is missing.
  3. Run it from anywhere. Typing ruff check in any folder finds the command on PATH and runs ruff from its own environment, so the same ruff checks every project.
  4. Your project stays as it was. Your project's .venv is untouched. ruff cannot be imported there, and nothing ruff needs can clash with the versions your project uses.
  5. The same with uv. uv tool install ruff does the same job with uv, and uvx ruff check runs ruff once in a temporary environment without installing it at all.

Task With pipx With uv
Install a tool and put its commands on PATH pipx install ruff uv tool install ruff
Run a tool once, without installing it pipx run ruff check uvx ruff check
Upgrade an installed tool pipx upgrade ruff uv tool upgrade ruff
Make sure the tools’ folder is on PATH pipx ensurepath uv tool update-shell

pipx itself comes from a package manager (brew install pipx on macOS, sudo apt install pipx on Ubuntu, scoop install pipx on Windows) or, on Windows, from py -m pip install --user pipx. uvx is short for uv tool run. A tool installed this way is not importable by your own code: it lives in its own environment, which is the point. A tool that has to import your project, such as pytest running your tests, belongs in the project instead, as a development dependency (uv add --dev pytest), and runs with uv run pytest.

Checkpoint

Check: Tools in environments of their own

  1. One question You installed Ruff with pipx install ruff. In your project's environment, what does python -c "import ruff" do?

    Choose one answer.

    Show the answer: One question

    Answer: It fails with ModuleNotFoundError, because Ruff lives in an environment of its own

    An application installed with pipx or uv tool gives you its command, not its modules: its environment is its own, which is what keeps its dependencies from clashing with yours.

Choose the tool by the job

All three do the same steps underneath: a virtual environment, an install, a record of versions. They differ in how much of it they do for you and in what they are for:

venv and pip, uv or pipx?
Criterion venv + pipuvpipx
Comes with Python YesNo: install it onceNo: install it once
Creates virtual environments Yes, when you askYes, by itself, for a project or a scriptYes, one for each application
Can install Python itself NoYesYes, for an application, if you ask
Records exact versions pip freeze > requirements.txtuv.lock, exported as pylock.toml on requestNot for projects: it manages applications
When to choose Any computer, with nothing extra to installProjects and scripts with dependenciesCommand-line applications you use everywhere

Read the table from the job you have. On a school computer where you may install nothing but Python, venv and pip are what you have, and they are enough. A team project with dependencies and a test run on every change is uv’s job, with uv.lock committed. A linter you use in every folder goes to pipx or uv tool, and a script you run once a month carries its own block and runs with uv run.

None of these replaces knowing venv and pip: uv and pipx automate the same steps, and when one of them does something unexpected, the previous lesson’s commands are how you look inside.

TOML ↔ JSON Converter Paste a pyproject.toml, or the TOML of a script block, to check it and see it as JSON.

Checkpoint

Check: Choosing venv and pip, uv or pipx

  1. One question On school computers you may install nothing but Python itself. A project there needs two packages from PyPI. Which way works?

    Choose one answer.

    Show the answer: One question

    Answer: python3 -m venv .venv, then .venv/bin/python -m pip install the two packages

    uv and pipx save work but have to be installed once; venv and pip are part of Python and do the same steps on any computer that has Python.

Practise: read a script’s metadata

Write the part of a script runner that reads the block: return its TOML as a dict, None when there is no block, and raise ValueError for a script with two. The sample tests run in your browser:

Exercise · Medium · Python

Read a script's inline metadata

Write parse_script_metadata(text). text is the source of a Python script. Return the TOML of its # /// script block, read with tomllib.loads(), as a dict, following these rules of the specification:

  • The block starts with a line that is exactly # /// script and ends with a line that is exactly # ///.
  • Every line in between is a comment: take away its first two characters, # and a space. A line that is only # becomes an empty line.
  • A script without a block gives None.
  • A script with two script blocks is refused: raise ValueError.

Starter code · script_meta.py

import tomllib


def parse_script_metadata(text):
    """Return the TOML of the script's '# /// script' block as a dict, or None if it has none."""
    return None
The sample tests · test_script_meta.py
from script_meta import parse_script_metadata

BLOCK = """# /// script
# requires-python = ">=3.14"
# dependencies = [
#   "rich",
# ]
# ///
print("Report ready")
"""


def test_one_block():
    """reads the TOML of the block"""
    assert parse_script_metadata(BLOCK) == {"requires-python": ">=3.14", "dependencies": ["rich"]}


def test_no_block():
    """gives None for a script without a block"""
    assert parse_script_metadata('print("Report ready")\n') is None


def test_two_blocks():
    """refuses a script with two script blocks"""
    try:
        parse_script_metadata(BLOCK + BLOCK)
    except ValueError:
        return
    raise AssertionError("a script with two script blocks must raise ValueError")


def test_empty_dependencies():
    """reads a block whose dependencies list is empty"""
    assert parse_script_metadata("# /// script\n# dependencies = []\n# ///\n") == {"dependencies": []}


def test_line_of_only_a_hash():
    """treats a line that is only # as an empty line of the block"""
    text = '# /// script\n# requires-python = ">=3.14"\n#\n# dependencies = []\n# ///\n'
    assert parse_script_metadata(text) == {"requires-python": ">=3.14", "dependencies": []}
A hint

Loop over text.splitlines() with a variable that holds the lines of the block you are in, or None outside a block. The opening line starts a new list; # /// closes it and adds it to a list of finished blocks. After the loop, count the finished blocks: none gives None, one goes to tomllib.loads("\n".join(...)), more raise ValueError.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Finish with the questions below, which follow your answers: a miss brings a simpler question on the same idea, and each choice carries its own explanation:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 Which of these files does uv init weather-notes not create?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: uv.lock

    uv init writes pyproject.toml, .python-version, README.md, a .gitignore, starter code and a Git repository. uv add, uv run and uv sync create uv.lock and .venv the first time they run.

  2. Question 2 of 10 You pull a teammate's change that adds a package to pyproject.toml and uv.lock. Which command makes your .venv match before you start work?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: uv sync

    uv sync makes the environment match the lock exactly. Because uv run checks the same thing before every run, a uv run would bring .venv up to date too.

  3. Question 3 of 10 What does uv.lock record that pyproject.toml does not?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: The exact version of every package installed, the dependencies of your dependencies included

    A range such as rich>=15.0.0 can match many versions. uv.lock records the one that was chosen, and the versions of rich's own dependencies, such as markdown-it-py and Pygments.

  4. Question 4 of 10 Your project's lock holds tablekit 2.3.0. The index now offers 2.4.0, its newest version, and you want everyone to move to it once it has passed your tests. What do you do?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: Run uv lock --upgrade-package tablekit, run the tests, and commit the new uv.lock

    Upgrades are deliberate: uv lock --upgrade-package moves one package to its newest allowed version, the tests show whether it works, and the committed lock carries the change to everyone.

  5. Question 5 of 10 What does plain python3 report.py do with the # /// script block at the top of report.py?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: Nothing; to Python the block is only comments, so the import fails if rich is not installed

    The block is a set of comments, which Python skips. That is why the recorded run of report.py stops with ModuleNotFoundError, while uv run report.py would first build an environment with rich.

  6. Question 6 of 10 What does read_metadata.py, the program stepped through above, print?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: requires-python: >=3.14 dependencies: ['rich']

    Between the opening and closing lines the loop collects the TOML lines, and tomllib turns them into a dict: the string >=3.14 and the list ['rich'].

  7. Question 7 of 10 Which of these belongs in pipx or uv tool rather than in a project's environment?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: A command-line application you use in every folder, such as Ruff

    pipx and uv tool are for applications used as commands. Libraries, and tools that need your project installed, go into the project's environment.

  8. Question 8 of 10 What does uvx ruff check do?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: It runs Ruff once in a temporary environment, without installing it

    uvx, short for uv tool run, runs a tool in a temporary, isolated environment: handy for a tool you use now and then. uv tool install keeps it for good.

  9. Question 9 of 10 A one-file script that you run once a month needs rich. What fits it best?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: A '# /// script' block that lists rich, run with uv run or pipx run

    For a single file, inline script metadata keeps the dependencies with the code, and uv run or pipx run builds the environment each time it is needed.

  10. Question 10 of 10 You want Ruff's command in every terminal, without touching any project. How do you install Ruff?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: pipx install ruff, or uv tool install ruff

    Command-line applications that you want everywhere are what pipx and uv tool are for: one isolated environment each, and only the commands on PATH.

Interview questions

Warm-up (fresher): what is the difference between pyproject.toml and uv.lock?

pyproject.toml is the project’s own description, the file you edit: its name and version, the Python versions it supports, and its dependencies as ranges such as rich>=15.0.0, which say what would be acceptable. uv.lock is written by uv: the exact version of every package the project installs, its dependencies’ dependencies included, so that every computer and every later install gets the same set. You change pyproject.toml yourself or with uv add, and you never edit uv.lock by hand; both are committed. When you want newer versions, you upgrade the lock on purpose, with uv lock --upgrade-package, and test the result before you commit it.

Two harder questions start from this lesson: a build that broke over a weekend without a code change, and a team whose tools are installed in the wrong places. Work out your first step before you open each one:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    Your CI installs a project with pip install . from pyproject.toml, whose dependencies are ranges such as tablekit>=2.3. On Monday the build fails with AttributeError where your code calls tablekit, though nothing changed since Friday's green build. Diagnose it, and decide what to commit and run so it cannot happen again.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A new teammate installed Ruff, pytest and the project's code generator with pipx. Ruff works, but pytest cannot import the project, and the generator fails with ModuleNotFoundError for a package the project uses. Which tools belong in the project's environment and which in pipx or uv tool, and why?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • uv manages a project from pyproject.toml: uv init, uv add and uv run, and before every run it checks that uv.lock and .venv are up to date.
  • pyproject.toml holds ranges of acceptable versions; uv.lock records one exact set, dependencies included, and is committed so that everyone installs the same packages.
  • Inline script metadata is a # /// script comment block with requires-python and dependencies; uv run and pipx run read it, while plain python3 ignores it.
  • pipx install and uv tool install give each command-line application its own environment and put only its commands on your PATH; tools that import your project belong in the project.
  • uv and pipx automate the steps of venv and pip, which still work on every computer.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.