Building a single-file portable Python script with embedded dependency management

When you want to write a “small script” in Python, the moment you try to use external libraries it becomes a hassle. You have to create a pyproject.toml, install dependencies with uv add (or uv sync), and ideally also set up a virtual environment… that whole process.
There’s a way to make it self-contained in a single Python file. Use the combination of PEP 723 (Inline script metadata, accepted in 2024) and uv.
Final form
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["jinja2>=3.1", "requests>=2.31"]
# ///
"""Render a Jinja2 template fetched from a URL."""
import sys
import requests
from jinja2 import Template
resp = requests.get(sys.argv[1])
print(Template(resp.text).render(name="world"))
Just save this and run chmod +x script.py, then you can execute it with:
./script.py https://example.com/template.j2
No pip install, no python -m venv. Even if Python itself isn’t installed, uv will download it automatically.
Line-by-line explanation
The shebang line
#!/usr/bin/env -S uv run --script
#!= Unix shebang. The kernel looks at the beginning of the file and interprets it as “run this with …”/usr/bin/env -S= The-Soption for theenvcommand. It means “it’s okay to split shebang arguments into multiple tokens using spaces.” Without it,uv run --scriptis interpreted as a single executable name, resulting incommand not found.- Supported on macOS and Linux 4.0+
- Doesn’t work on older environments such as Ubuntu 18.04 and earlier
uv run --script= Tells uv: “run this file in standalone script mode”
The PEP 723 metadata block
# /// script
# requires-python = ">=3.11"
# dependencies = ["jinja2>=3.1", "requests>=2.31"]
# ///
This is the inline script metadata syntax standardized by PEP 723.
- Lines inside the block delimited by
# /// scriptand# ///are parsed as TOML, after stripping the#prefix from each line - Since it’s written as ordinary comments, tools that don’t support PEP 723 (older IDEs, linters, etc.) will ignore it as just comments. Compatibility isn’t broken.
- The fields follow the same schema as the
[project]table inpyproject.tomlrequires-python= required Python version (PEP 440 format)dependencies= dependency packages (PEP 508 format)
What happens at runtime
The flow when running ./script.py:
- The kernel reads the shebang and starts
/usr/bin/env -S uv run --script /path/to/script.py - uv parses the
# /// scriptblock at the top of the file as TOML - uv creates an isolated virtual environment dedicated to this script under
~/.cache/uv/(first run only; cached based on the script hash) - uv installs the packages specified in
dependenciesinto that environment (first run only) - uv looks for a Python that satisfies
requires-python. If none is found, uv downloads one automatically - uv runs
script.pyinside that isolated environment
Comparison with the conventional approach
| Item | Conventional (uv project workflow) | inline metadata + uv shebang |
|---|---|---|
| Dependency management | Create pyproject.toml separately |
Self-contained in one script file |
| Virtual environment | Create with uv venv etc. |
uv auto-creates and caches |
| Install | uv add jinja2 / uv sync |
Automatic on first run |
| Python version management | Specify via .python-version etc. |
uv auto-fetches |
| Run | uv run python script.py |
Run directly with ./script.py |
| Distribution | Share the whole project directory | One file is enough (email attachment / Gist) |
Benefits in numbers
- First run: ~3 seconds (including dependency download and Python acquisition)
- Subsequent runs: ~200 ms (cache hit)
- Portability: A single self-contained script file. Works as one Gist, an email attachment, or a Slack paste
- Zero environment pollution: Nothing is installed into global Python; it runs in an isolated environment
Pitfalls
1. env -S doesn’t work on older Linux
The -S flag was introduced in coreutils 8.30 (2018). It can’t be used on Ubuntu 18.04 and earlier, CentOS 7, etc.
Workaround: omit the shebang and launch explicitly with uv run --script script.py.
2. uv must be installed
curl -LsSf https://astral.sh/uv/install.sh | sh
uv itself is distributed as a single binary, and installation is lightweight (written in Rust).
3. Cache directory permissions
It will fail if there is no write permission to ~/.cache/uv/. This can be an issue in CI environments, containers, or sandboxed environments.
You can change it via an environment variable:
UV_CACHE_DIR=/tmp/uv-cache ./script.py
4. The IDE won’t recognize dependencies
VSCode’s Pylance and similar tools may not be able to find the virtual environment resolved by uv, so warnings like Import "jinja2" could not be resolved may appear.
Mitigations:
- Ignore the warning (it won’t affect execution)
- Add
# pyright: reportMissingImports=falseat the top of the file - Create a separate environment for the IDE with
uv venv && source .venv/bin/activate && uv pip install jinja2
5. Few tools support PEP 723 yet
Besides uv, Hatch and pdm are working on support. pip alone cannot interpret this feature.
Use cases
Good fits:
- One-off scripts you want to distribute (internal tools, snippets shared via Gist)
- Helper scripts used only in CI
- Running “that processing” generated by ChatGPT / Claude as a single file
- Sample code you paste into blog posts
- Light distribution of a small custom CLI tool that has dependencies
Not a good fit:
- Full applications split across multiple files (use
pyproject.tomlnormally) - Cases where you want to minimize startup latency as much as possible (though post-cache it’s ~200 ms, which is fine for most use cases)
Summary
For a long time, Python has been criticized for “not being runnable with just a single shebang line” and “being annoying to set up when there are dependencies.”
With the combination of PEP 723 and uv, it becomes natural to do in Python what Bash and Node.js have long done by default: an executable script that is fully self-contained in a single file.
For small automation scripts, it’s reasonable for this to become the default.
References
We look forward to discussing your development needs.