This is kind of a follow-up to the older thread about locking PEP 723 single-file scripts, but that topic got a bit off-topic towards the end & has been quiet since last August so I thought it would be better to make a new one than do some forum necromancy.
The thing I keep coming back to is that the “single-file script you can distribute” use-case is one of the things Python is great for (if you can restrict yourself to “things that are in the stdlib on debian stable” until PEP 723 is more widely implemented/available
)
Often I want to write a script that does more complex things than you can reasonably/sanely do in a shellscript, but I also want to be able to share it around with others without having to build a package/make a project directory/publish build artifacts somewhere, etc. - I just want a .py file i can read/edit/copy around/send to friends/put in a single-file github gist/run.
At the same time, if I’m going to be sharing a script with others and I’ve been freed from “stdlib only” by PEP 723, i’d really like to have the dependencies locked, so that when $friend tells me it’s throwing an error on their system I can have some level of confidence that it’s my code which is the problem.
Having a script.py.lock next to script.py does work okay for this, but means the script isn’t really self-contained now - I have to remember to scp/curl/send/whatever both files, and if i don’t - or $friend only downloads the .py because the .lock looks funny or whatever - I don’t get any warning or indication that there should’ve been a pylock present.
Zipapps are neat, but if we’re being honest they’re also kind of cursed. they’ve been known to make security tooling very upset, some multi-user environments block them administratively for security reasons, and a surprisingly large number of users don’t even know they exist. They’re also not directly editable, or particularly convenient to assemble.
The obvious thing would be “oh well just put the lock data in the PEP 723 block”, but that ends up being really ugly. The metadata block is nice because it’s this short, human-readable thing; Python version, dependencies, maybe a few tool option blocks. But a lockfile is a pile of machine-generated/machine-managed stuff full of hashes, wheel names, markers, environment support flags, etc - it’s useful! but it’s also nigh-unreadable and far from concise. I would prefer to not have to scroll past a thousand-plus lines of resolved lockfile just to edit my actual script.
So to get to the point: Would it make sense to support embedding a pylock.toml block as a trailer at the end of a PEP 723 script?
Something like:
#!/usr/bin/env python3 --script
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "rich",
# "typer",
# ]
# dependency-lock = "inline"
# ///
# actual script content goes here
# /// pylock
# lock-version = "1.0"
# environments = [
# "sys_platform == 'linux'",
# "sys_platform == 'darwin'",
# "sys_platform == 'win32'",
# ]
# requires-python = ">=3.12"
# ... standard pylock.toml contents ...
# ///
I’m not married to dependency-lock = "inline", especially if extra top-level fields in the PEP 723 block are not acceptable. Maybe the # /// pylock marker is enough and tools can just discover it directly. A related sidecar-lockfile marker like dependency-lock = "file" might also be useful, but that is scope creep and I will probably regret mentioning it.
The exact marker is not the hill I care about, though. The part I care about is the UX - one file, easily human-readable/editable, embedded resolved lockfile at EOF where it’s out of the way, no zipapps, just “you can run this script and have some amount of confidence that it will use these exact dependencies, or throw an error”.
A tool could parse the PEP 723 block, find the trailing pylock/pylock.toml block, check that it still matches the script metadata, create/reuse a cached env, install from the lock, and run the script without doing resolution. That seems like it would preserve the nice properties of PEP 723 single-file scripts while still allowing them to be properly locked. You get one normal editable .py file, not a directory, not a sidecar lockfile, and not a zipapp/build artifact.
Is there a reason that this is a non-starter overall? I’ve done some digging around and I didn’t find it suggested elsewhere - could be a skill issue on my part, apologies if so - or has it just really not been explored?