Interpreters, tools, and scripts that carry their own dependencies
Two things uv does that have no equivalent in the JavaScript toolchain you know. Both remove a whole category of setup instructions, and the second one changes what a script can be.
uv manages the interpreter
pnpm does not install Node. uv installs Python.
uv python list # installed and available, side by side
uv python install 3.14 # fetch a standalone build
uv python pin 3.14 # write .python-version for this project
uv run --python 3.13 app.py # override for one invocationThe versions uv installs are standalone builds, not your system Python and not touching it. Your OS package manager keeps its Python, asdf or pyenv keeps theirs if you have them, and uv’s live under its own directory.
Which interpreter a project gets is decided by two files:
.python-versionnames the default version for this project. Committed.requires-pythoninpyproject.tomlconstrains the acceptable range, and is also what a consumer of your published package sees.
If the named version is not installed, uv installs it rather than failing. That single behaviour deletes the paragraph at the top of every Python README that begins “first, make sure you have Python 3.x”. A contributor clones, runs one command, and gets the right interpreter and the right packages.
uvx runs a tool without installing it anywhere
uvx ruff check .
uvx --from httpie http GET example.com
uvx pycowsay hellouvx is pnpm dlx. It resolves the tool into a cached environment, runs it, and leaves your project alone. The --from form is for when the package name and the command name differ, which is common.
For a tool you want permanently on your PATH, uv tool install ruff is pipx install ruff, and each tool gets its own isolated environment so two tools can depend on incompatible versions of the same library without a fight.
The distinction worth holding:
| Command | Environment | Use when |
|---|---|---|
uv run x |
The project’s environment | x is a declared dependency of this project |
uvx x |
An ephemeral cached environment | One-off, or a repo you are only reading |
uv tool install x |
A persistent isolated environment | You want x on your PATH everywhere |
uv run ruff on a project that has not declared ruff fails, and that failure surprises people because uv run looks like the universal runner. It is the project runner. uvx is the universal one.
Scripts that declare their own dependencies
This is the feature with no analogue anywhere in JavaScript, and it is genuinely excellent.
uv init --script fetch.py --python 3.14
uv add --script fetch.py httpxThat writes a comment block at the top of the file:
# /// script
# requires-python = ">=3.14"
# dependencies = [
# "httpx>=0.28.1",
# ]
# ///
import httpx
def main() -> None:
print(httpx.get("https://example.com").status_code)
if __name__ == "__main__":
main()Then uv run fetch.py reads that block, resolves it, builds a cached environment, and runs the script. There is no pyproject.toml, no .venv beside the file, and no install step. The script is one file that you can email, paste into a gist, or drop in a repo that has nothing to do with Python, and it still runs with its three dependencies.
This is the shape to reach for whenever you are writing something that would have been a shell script. A twenty-line utility that needs httpx and rich no longer requires a project around it.
The rest of the modern toolchain, briefly
You will meet these in every current repo, and all of them configure from pyproject.toml.
| Tool | What it is | The one you knew |
|---|---|---|
| ruff | Linter and formatter, one Rust binary, extremely fast | eslint plus prettier |
| pyright | Type checker from Microsoft, powers Pylance in VS Code | tsc |
| mypy | The original type checker, stricter defaults in places, slower | tsc, older |
| pytest | The test runner. Plain assert, fixtures instead of setup hooks |
vitest |
| ty | Astral’s type checker, newer, very fast, still maturing | a faster tsc |
ruff is not a preference, it has effectively won: it replaced flake8, black, isort and a dozen plugins with one tool, and a new Python repo without it is a signal about the repo.
On type checkers: pyright and mypy will disagree with each other on real code, which is a genuine difference from the TypeScript world where there is one compiler and its answer is the answer. Pick one per repo, put it in [dependency-groups], and run it in CI.
Your working setup, end to end
uv init my-thing && cd my-thing
uv add langchain langgraph
uv add --dev pytest pyright ruff
uv run pytest
uv run ruff check --fix
uv run pyright
uv run python -m my_thingNothing activated, nothing installed globally, one lockfile committed, and a colleague can reproduce it from a clone plus one command.
Where this goes
Module 3 is types. This is the part where the honest comparison with TypeScript gets uncomfortable, and the part where Pydantic turns out to be doing something TypeScript genuinely cannot.
Try it yourself
The comment block at the top of a script
You open a colleague's report.py and the first six lines are comments containing # /// script and a dependencies list. What is that?
Show answer
Correct answer: B — PEP 723 inline metadata, a real standard that lets uv run report.py build the environment from the file itself
It is PEP 723, a genuine interoperable standard rather than a uv invention, which matters because other runners honour it too. uv run report.py reads the block, resolves it, builds a cached environment and runs the script, so a one-file utility can depend on three libraries and still be a single file you can email. Calling it a uv-specific comment convention is the most plausible wrong answer because the syntax genuinely IS just comments, so a plain python report.py ignores it entirely and fails on the import. That half is true; the conclusion that it is uv-only is not.
Choosing uvx over uv add
You want to run ruff against a repo you are only reading, and you do not want to modify its pyproject.toml. What is the right command?
Show answer
Correct answer: C — uvx ruff check, which fetches it into a cached ephemeral environment and leaves the project untouched
uvx is pnpm dlx: an ephemeral, cached environment for a tool you are not declaring. uv run ruff check is the sharpest distractor because uv run looks like the general-purpose runner and it is, but it runs commands in the PROJECT environment, so uv run ruff fails unless ruff is already a dependency. uv pip install ruff genuinely installs ruff and is the wrong instinct here: it mutates an environment belonging to a repo you were only reading, which is precisely the class of accident uvx exists to remove.
Where the interpreter comes from
The piece with no pnpm equivalent, and the one that removes an entire category of setup instructions.
How does uv decide which Python interpreter a project uses, and what happens if that version is not installed on the machine?
Reveal answer
The project's .python-version file names the version, requires-python in pyproject.toml constrains the acceptable range, and uv python pin writes the former. If the named version is not present, uv downloads and installs a standalone build itself rather than failing, so a contributor with the wrong Python still gets a working environment from one command. This is closer to what nvm or fnm does for Node than to anything pnpm does, and it is why a uv project needs no setup instructions about installing Python first. uv python list shows what is installed and what is available, and uv run --python 3.13 ... overrides for one invocation.
Write a one-file script that has dependencies
uv init --script fetch.py --python 3.14, then uv add --script fetch.py httpx, then write five lines that fetch a URL and print the status. Run it with uv run fetch.py.
The script runs, there is no pyproject.toml and no .venv beside it, and running python fetch.py directly fails on the import, which tells you where the environment actually came from.