I have seen articles and discussions recently that recommend a shebang for scripts that use inline script metadata (PEP 723). They currently link directly to a particular script runner, often uv. This allows running the script directly, but only when that particular script runner is installed. For example:
#!/usr/bin/env -S uv run --script
Shebangs in scripts that hard code assumptions have a history of being problematic for the Python community. PEP 394 was created during the Python 2 → 3 migration because the version of the python executable was ambiguous in shebangs, among other issues. Hard coding the existence of a given script runner would create a similar situation with a proliferation of scripts assuming one runner or another.
My proposal is to introduce an executable like python-script that can be used in a shebang to run a script with inline script metadata. The shebang would then look like this:
#!/usr/bin/env python-script
The implementation mechanism would be left up to whatever entity creates the python-script executable.
I understand the problem but am confused about what you are proposing. Are you suggesting that the name of a tool, not included in Python, be standardized? But without an implementation, and then various tools will implement that same tool?
That seems problematic, and not only in technical ways. How could the different tool implementations be installed side by side? Won’t users get confused if the implementations diverge?
I don’t think this is the right way to proceed.
But I agree that it would be nice to have a tool-agnostic way of writing a shebang or script preamble to say “this script works if you have pipx, pip-run, uv, or any other script runner”.
It’s already possible to get clever with shutil.which and os.exec to reinvoke a script under a detected script runner. Can we develop best practices for writing that sort of script? Can it be standardized in some way? Can hard parts of it be solved in the stdlib?
This is already a de facto standard, except it’s any shebang containing the word python (the minimal, canonical example being #!python) rather than a specific line like #!/usr/bin/env python-script. The setuptools package started this, I think, and I don’t recall if this is unique to setuptools or if related projects have adopted it as well. At installation time, the shebang is rewritten to use the absolute path of the Python interpreter doing the installation.
The idea of leaving the implementation of python-script unspecified is that the script itself should only care that a PEP 723 compatible script runner is in use. Any number of implementations could be used:
a switcher that creates a wrapper script named python-script
a symlink that is detected by the script runner
an executable reads from some configuration (environmental variables?) to choose a script runner
I suppose there could a reference implementation.
Won’t users get confused if the implementations diverge?
I don’t understand this concern, assuming that a script is not adopting inline metadata features from an individual implementation. If it is adopting implementation-specific features, it would be appropriate to use a shebang for a specific script runner.
It’s already possible to get clever with shutil.which and os.exec to reinvoke a script under a detected script runner. Can we develop best practices for writing that sort of script? Can it be standardized in some way? Can hard parts of it be solved in the stdlib?
I would be curious about this, but it does pose a few problems. First, I’m not sure how detection would work. Wouldn’t that require a known set of script runners? Or are you thinking of pulling from some method of configuration? The other problem is that it requires a Python installation to bootstrap from. The Python installation would need to be very new if a new stdlib component was involved.
The python-script requirements could be satisfied in a way that best suits an environment or distro. At its very basic form, a user could satisfy the requirement themselves with a shell script like this at ~/.local/bin/python-script:
The #!python shebang and shebang rewriting requires a build or installation step. This use case involves directly running a PEP 723 script standalone without requiring modification. I’ve seen two threads on Reddit about this use case where people are using uv in the shebang. Here is one:
Okay, but which one are you proposing? If multiple Python packages try to provide this as a console entry point, they’ll conflict with one another.
So who provides this tool? And what does it do?
The exact treatment of even the standardized data is not strictly defined. For example, one script runner may implicitly lock dependencies while others do not. Some runners will implicitly fetch a new interpreter if a matching one is not found, while others may treat it as an error.
It’s not a deal breaker issue but I think it is a real one.
If all this means is that an arbitrary script runner will be invoked, then it’s a standard in service of non-portable or only somewhat-portable scripts.
Which is not to say it’s useless, but it makes me question whether or not it’s a good idea.
The only thing I can think to do today is to hardcore a list of known tools.
If tool runners made themselves discoverable via some kind of package metadata, other Python tools could be supported, but that doesn’t work for uv (in the absence of the Python packaged version of it).
But I like this idea better than proposing a vague standard for a script runner invoker. As it currently stands, I’m not sure if the idea you’re proposing is actionable – who does what with it? By contrast, writing a standard script preamble and talking about the problems with it could lead to some productive ideas about what tools or CPython can do.
I don’t really see this as a problem.
If you’re proposing a standard Python tool, then the user needs to get that tool from somewhere. So why should that place be somewhere different from getting the language itself?
If you want to propose a non-Python tool which handles bootstrapping, that’s fine, but what advantage does this tool offer over pyenv or uv? I find the idea much harder to grasp and discuss in this form.
If you want to just spitball ideas, that’s cool – pretty much what I’m doing here – but I’m struggling to figure out how we can discuss the idea of we don’t start to pin down some details or requirements for it.
Is it a specific tool? If it’s not a tool, is it an agreement from existing tools to do something (and if so, what are they agreeing to do)?