While it can also do much more, Python is a fantastic language for writing small
scripts and utilities with it’s expressive syntax and batteries-included
standard library. But what if you need just a bit more? PyPI is one of the best
package repositories for any language, and being able to access it without
having to write a multi-file library and setting up virtual environments would
be a dream - one that is becoming reality. Pipx 1.4.2 has an experimental
implementation of the provisionally accepted PEP 723, and I’d like to show
it off here, as it’s tremendously useful for simple scripts & utilities. Support
is also available in Nox 2024.04.15 and Hatch 1.10.
The solution
You can now write files that look like this:
# /// script
# dependencies = ["rich"]
# ///
import rich
rich.print("[blue]This worked!")
And run them with pipx run
(or hatch run
):
$ pipx run ./print_blue.py
This will check to see if it’s run a script with this environment before; if it
hasn’t, then it will make an environment with the listed run dependencies. Then
it will run the script in this environment. That’s it! (Remember to include the
./
to ensure pipx run
doesn’t try to find a PyPI package named
print-blue-py
).
This is great for one off scripts, for various tools, and even when sharing code such as in tutorials or Gists. You can specify exactly that the run requirements are in the script itself.
This inline dependency idea isn’t new; tools like pip-run
have provided custom
syntaxes for inline dependencies before. But this is finally providing a unified
syntax that we can all agree on, which will enable different tools and editors
to process these as well.
Also, this is just the beginning. There’s also a requries-python
field, which
is currently ignored by pipx, but in the future could influence what Python it
searches for or cause it to produce a (slightly better) error if it doesn’t
match. Other tools can read tool.<stuff>
fields in here; Ruff has expressed
interest in allowing pre-file configuration by setting tool.ruff
items here.
The problem
When writing any code, being able to use libraries provided by others is one of
the most important parts of programming. Without that, we will be stuck
rewriting the same bits of code forever, never advancing. But installing
libraries is hard - you really should be using virtual environments - one for
each independent piece of code (at least with unique requirements), and that’s
a lot of effort that currently is rarely done correctly. The most common
solution is to have a “dev” environment, either defined in requirements.txt
,
or in a [dev]
extra. And all scripts have all requirements in this one place,
hoping that there are no collisions (black and tensorflow were not installable
in the same environment at one point, for an example). This doesn’t work very
well if this isn’t already a package, though, and still requires a lot of
special knowledge to set up (like the location of the requirements.txt
file.
Previous solutions
An older solution to this is using a task runner, like tox, nox, or hatch. These
allow multiple environments and make management trivial. But they split the
environment specification into multiple files (tox.ini
, noxfile.py
, or
pyproject.toml
), while the solution above has fully self contained single file
scripts. For example, in nox, the above example would require the following:
noxfile.py
:
import nox
@nox.session()
def print_blue(session: nox.Session) -> None:
"""
Runs the print_blue script.
"""
session.install("rich")
session.run("python", "print_blue.py", *session.posargs)
(Technically you could cut a few bits of this due to the simplicity of the script in our example) and you would run it with:
$ nox -s print_blue
Now, with Nox 2024.04.15, you can use:
import nox
@nox.session
def print_blue(session: nox.Session) -> None:
"""
Runs the print_blue script.
"""
deps = nox.project.load_toml("print_blue.py")["dependencies"]
session.install(*deps)
session.run("python", "print_blue.py")
Which will read the PEP 723 metadata for you. A future version might further simplify, but this allows full freedom in designing custom sessions.
tox.ini
:
min_version = 4.0
[testenv:print_blue]
description = Runs the print_blue script.
skip_install = True
deps = rich
commands = python print_blue.py {posargs}
(Technically you could cut a few bits of this due to the simplicity of the script in our example) and you would run it with:
$ tox -e print_blue
pyproject.toml
:
[tool.hatch.envs.print_blue]
description = "Environment for the print_blue script."
detached = true
dependencies = ["rich"]
scripts.print_blue = "python print_blue.py {args}"
(Technically you could cut a few bits of this due to the simplicity of the script in our example - also, you don’t need to “export” the script like this) and you would run it with:
$ hatch run print_blue:print_blue
In Hatch 1.10, you can now just run hatch run print_blue.py
if you use PEP
723!.
Conclusion
I’d highly recommend trying it out, and seeing what you think. I think this will transform the way we write small scripts in Python, and I’m looking forward to seeing Python packaging continuing to get simpler. Rust’s Cargo also recently got am inline script dependencies as a nightly feature, too.
If you are interested in dev dependencies for projects that do have
pyproject.toml
s, also see PEP 735, has been proposed to cover dependency groups.