(Y’all can thank @brettcannon (no relation))
So, are you going to propose a “lock file” comment for single-file scripts?
I’m fishing for thoughts here, then will collate them into a PEP.
Background
AllMost of the same motivations for “why lockfiles” applies as much to a PEP 723 single-file script (is that the official nomenclature?)- But currently, single-file scripts aren’t easily “lockable”
This is primarily motivated by wanting to distribute single-file scripts (distribution of a single-file-runnable is something the broader ecosystem likes to focus on, because of the simplicity of distribution. Rust and Go compile down to a single binary. Pex and shiv and pyapp and PyOxidizer all attempt to do the same for Python).
- Pex: Single-file executable (zipapp) with transitive dependencies and the bootstrapping script baked-in
- Shiv: Single-file executable (zipapp) with transitive dependencies baked-in
- PyApp: Single-file executable (binary) with transitive dependencies (and the bootstrapping logic) baked-in
- PyOxidizier: Single-file executable (binary) with transitive dependencies (and the bootstrapping logic) and Python baked-in
Taking an “outside-in” approach to the same set of problems these tools attempt to solve:
uv run --script <script with locked deps>
Single file “executable” (especially if you use
#! /usr/bin/env uv run --script
(With this PEP:
) Fully pinned transitive dependencies AKA “fully deterministic/reproducible environment”
Brings-its-own-Python (via
uv
’s Python binaries support)- BONUS:
Supports “remote” executables (
uv run --script <url>
) - (it don’t get much easier than that)
As an example of this kind of simplicity in distribution, all this is missing is guaranteed reproducibility:
uv run -q --refresh https://raw.githubusercontent.com/thejcannon/joshcannon.me/refs/heads/main/scripts/claudesay.py 'Certainly!'
Things to bikeshed over
Now that that’s covered, let’s get arguin’!
First and foremost, should we try and shove this into the PEP 723 metadata block?
Right now the # /// script
block supports:
requires-python
(as doespylock.toml
with the same semantics)dependencies
(as doesn’tpylock.toml
)tool
(as doespylock.toml
with the same semantics)
That means right now we could completely overlay the existing keys and all of pylock.toml
keys and have no conflicts.
This would look something like:
# /// script
#
# # Optional. Doesn't conflict with `[[ packages ]]` because it could live as the "input" set of dependencies
# dependencies = [...]
#
# # and/or
#
# lock-version = "..."
# [[packages]]
# <a bunch o' lockfile metadata>
# ///
We could probably scope down the possible keys, which would also help this (see “What’s in it?” below)
This is honestly my vote, but keep in mind I’m biased. I don’t maintain any Python packaging tool or standards or official docs.
Should we use a new format?
What’s the “tag”?
E.g. # /// pylock
? # /// scriptlock
?
My vote: pylock
Can a file have both?
My vote: yes, (i.e. how a project has a pyproject.toml
and a pylock.toml
) and we should require tools read locked metadata over script if both exist.
Where does it go?
- Does its location relate to the
# /// script
block if it exists? (E.g. is it required to go beneath a potential# /// script
block?)- My vote: (I don’t care. I have preferences but not requirements)
- Can it go anywhere in file (a la PEP 723)? (E.g. is it required to be at the bottom?)
- My vote: (I don’t care. I have preferences but not requirements)
What’s in it?
For simplicity, we could let it just have the same schema/spec as pylock.toml
.
That being said, keys like environments
and dependency-groups
doesn’t make as much sense here and so might cause confusion/abuse.
At a minimum I’d argue we’d need:
lock-version
requires-python
[[packages]]
(and all of the keys under it)[tool]
Lets get crackin’