Code of the Day
AdvancedCross-Platform

Shebang and executables

How

UtilitiesAdvanced5 min read
By the end of this lesson you will be able to:
  • Explain what the shebang line does and why /usr/bin/env python3 is preferred over a hard-coded path
  • Describe why shebangs are ignored on Windows and how pip works around this
  • Use [project.scripts] in pyproject.toml to let pip generate the correct launcher for each platform

A Python script is just a text file. Making it directly executable — so users can run mytool instead of python3 mytool.py — requires different mechanisms on Unix and Windows. pyproject.toml entry points let you declare the intent once and let pip handle the platform-specific details.

The shebang on Unix

On Linux and macOS, the kernel reads the first two bytes of a file when it is executed. If they are #!, the rest of the line is treated as the interpreter path:

#!/usr/bin/env python3

/usr/bin/env python3 asks the env utility to find python3 in the current PATH. This is correct for virtual environments: if the user has activated a venv, python3 resolves to the venv's interpreter, not the system one.

A hard-coded path like #!/usr/local/bin/python3 breaks whenever the interpreter is installed elsewhere — which is common across Linux distributions, Homebrew on macOS, and pyenv. Always use env.

The file also needs execute permission:

chmod +x mytool.py
./mytool.py

Shebangs on Windows

The Windows kernel does not read shebang lines. Running ./mytool.py on Windows opens the file in the associated application (usually a text editor), not the Python interpreter. The Python Launcher for Windows (py.exe) does understand shebangs when you run py mytool.py, but that is not the same as a bare executable invocation.

More importantly, the shebang does nothing when the script is installed as a package — users still have to know to type python -m mytool or python mytool.py rather than just mytool.

Entry points: the portable solution

[project.scripts] in pyproject.toml declares a command-line entry point:

[project.scripts]
mytool = "mytool.cli:main"

When pip install mytool runs:

  • On Linux/macOS: pip writes a small script file to ~/.local/bin/mytool (or the venv's bin/) with a #!/usr/bin/env python3 shebang and a call to mytool.cli:main. It sets the execute bit automatically.
  • On Windows: pip writes mytool.exe and mytool-script.py (plus a mytool.cmd batch file) to the Scripts directory. The .exe launcher calls the Python script correctly without needing a shebang.

The result: mytool works as a bare command on all three platforms. You write the shebang once in the pyproject.toml declaration; pip materialises the right launcher for the current OS.

The Windows .cmd wrapper also handles the case where Scripts/ is on the PATH but the user is running from Command Prompt rather than PowerShell. The mytool command works identically in both shells.

What this means in practice

Two rules cover almost everything:

  1. Include #!/usr/bin/env python3 at the top of any script you distribute standalone (e.g., a single-file utility checked into a repository). It costs nothing on Windows and makes the file executable on Unix.
  2. For installable packages, declare commands in [project.scripts] and let pip handle the launchers. Do not write your own shebang management, .bat files, or shell wrappers — pip already does this correctly for every platform.

The pattern generalises to GUI applications ([project.gui-scripts]) and to the plugin entry points covered in the Plugin Systems module — all use the same underlying mechanism.

Where to go next

You have completed the Cross-Platform module. The advanced utilities track continues with the Plugin Systems module if you have not worked through it, or move to the workflow track to apply these patterns in CI/CD pipelines.

Finished reading? Mark it complete to track your progress.

On this page