#!/usr/bin/env python3 is the most correct general solution. On systems where Python 3 is installed, this should work, regardless of whether Python 2 is installed or not.
That said, there’s still no guarantee that the right version of Python 3 is installed, and hard-coding, say, python3.7 isn’t viable since most users won’t have several versions installed.
Luckily there’s a better solution: If you distribute your application as a Python package with Setuptools and specify entry_points for your binary/binaries, the installation process will pick the correct interpreter and hard-code its path into your application scripts. This will work regardless of environment, as long as the installer (e.g. pip) can find Python at all.
As a very simple example, say your project example has the following folder structure:
example
├── example
│ ╰── __init__.py
├── pyproject.toml
╰── setup.cfg
And let’s assume your __init__.py has a function main that we want to use as an entry point for an executable script.
Then pyproject.toml would look like this:1
[build-system]
requires = ["setuptools", "wheel"]
And setup.cfg looks as follows:1,2
[metadata]
name = example
version = 0.0.1
[options]
packages = find:
[options.entry_points]
console_scripts =
example-bin = example:main
Now you can build your project using e.g. pip3 bdist path-to-project, which will generate the Wheel installation bundle. When you install that (either after uploading it to PyPI or locally (via pip3 install wheel-filename.whl), pip3 will install the binary example-bin.
example-bin will launch your package’s entry point function, and will work regardless of what the Python 3 binary is called on your system, because pip3 install … creates this file with an absolute path in the shebang line. For example, on my system the first line of that file is
#!/usr/local/opt/python/bin/python3.7
1 Rather than use pyproject.toml and setup.cfg, the same works using the legacy setup.py file — but using the above is simpler.
2 Note that, at the time of writing, there’s a typo in the Setuptools quickstart: instead of [entry_point], it should read [entry_points].