Windows Python Installer not changing PATHEXT

I had an issue with executing the Python scripts from the PowerShell console - the script always opened a new console window. When I wrote .\script.py in Command Prompt (cmd.exe), it was executed in the same window, but in PowerShell console a new window was always opened. It used file association "C:\WINDOWS\py.exe" "%L" %* and when I tried to run it directly (py.exe .\script.py), it was executed in the same window even in PowerShell console. I reported it to PowerShell guys, and they recommended to set PATHEXT to add ;.PY at the end. So, I did it and the Python script started to run correctly in the same console window. I checked the installer documentation and there is a note that you need to check “Add Python X.Y to PATH”, which I did not have checked. I just have the system-wide launcher installed. So I think it makes sense to modify PATHEXT when the launcher is installed, not only when the PATH is modified.

This doesn’t belong in the packaging section, as it relates to the core distribution. But please report it on GitHub - python/cpython: The Python programming language so the idea is recorded.

1 Like

Currently, the installer only modifies PATHEXT for an all-users installation, which has administrator access. Setting PATHEXT in the user environment would replace the system value. It’s possible to prepend the system value by setting a REG_EXPAND_SZ value such as “%PATHEXT%;.PY;.PYW”. However, I haven’t ever seen this done, and it complicates maintenance a little bit.

Also, what about the app distribution on the Microsoft Store?


Background

Back in the 1990s, the CMD shell introduced the use of PATHEXT as a list of optional file extensions to append when searching PATH. In CMD, PATHEXT has nothing to do with which files are allowed to be executed, nor in what manner they’re allowed to be executed.

To run a file, CMD first tries CreateProcessW(). If the latter fails due to either ERROR_BAD_EXE_FORMAT or ERROR_ELEVATION_REQUIRED, CMD falls back on the shell API. It calls ShellExecuteExW(), for which it uses the flag SEE_MASK_NO_CONSOLE in order to avoid creating a new console session. If CreateProcessW() fails with any other error code, CMD stops and reports the error to the user, such as ERROR_ACCESS_DENIED. CMD thus relies solely on file security to determine execute permission[1][2].

PowerShell uses PATHEXT to restrict the file types that can be executed via CreateProcessW()[3]. If the file type is directly supported by CreateProcessW() (e.g. “.exe”, “.com”, “.bat”, “.cmd”), then its execute permission is honored. Otherwise PowerShell uses the shell API function FindExecutableW() to get the path of the associated executable, and it constructs a new command line[4]. In this case the file’s execute permission is ignored.

If the file type isn’t in PATHEXT, then PowerShell uses ShellExecuteExW()[3:1], which ignores the file’s execute permission except for directly executable file types (e.g. “.exe”). Also, unlike CMD, PowerShell calls ShellExecuteExW() without using the flag SEE_MASK_NO_CONSOLE, and thus a new console session is created.


  1. Files usually inherit inherit discretionary access control that grants execute access to users (or a single user in a profile directory), as well as implicit mandatory access control that allows execution at medium integrity level. There is no technical reason for this relaxed security. However, the way CMD is implemented is problematic, since it doesn’t try ShellExecuteExW() if CreateProcessW() fails due to a permission error. ↩︎

  2. One could argue that this is a mistake when running a script file (it’s certainly a mistake when opening a data file) by taking the point of view that FILE_EXECUTE permission on a regular file only relates to the right to map the file as an image section object with page execute access for section views – at least as far as the system API is concerned. According to the documentation, whether to honor FILE_EXECUTE access on a script file is left up to the interpreter. (Python doesn’t check this.) That said, CreateProcessW() requires execute access to run a batch script, but running "cmd.exe /c <script>" does not, so they’re inconsistent in how this is applied. ↩︎

  3. In actuality, PowerShell uses the .NET Process.Start() method with a ProcessStartInfo record, but ultimately it ends up calling either CreateProcessW() or ShellExecuteExW(). ↩︎ ↩︎

  4. PowerShell naively assumes that the command line should be of the form “{executable} {filename} {arguments}”. But an associated application may require particular command-line options to open the given file type. To implement this properly, PowerShell should interpolate the associated command template string that’s returned by AssocQueryString(): ASSOCSTR_COMMAND. ↩︎

2 Likes

The Microsoft Store version of Python is not usable for me, Google Cloud CLI cannot run with it. There are known limitations for the Store app: 4. Using Python on Windows — Python 3.11.1 documentation

Thanks for very detailed answer. Do you think I should create an issue on the PowerShell Core GitHub Issue Tracker to use CreateNoWindow in the Process.Start(ProcessStartInfo) call?

I was asking about the situation for users of the store app. I think the most we can reasonably do is to expand the documentation to explain in detail how to manually add “.PY” to the PATHEXT environment variable, and to explain why it’s needed.

No, the CreateNoWindow setting translates to the CREATE_NO_WINDOW process creation flag. This flag spawns the process in a new console session that has no user interface (i.e. no terminal window).

The Process class in .NET does not support the shell API flag SEE_MASK_NO_CONSOLE, which prevents creating a new console session. Even if it did, the PowerShell team probably think that it’s appropriate to a create a new console session if the file type isn’t in PATHEXT. I don’t agree with how PowerShell (and I think also MSYS bash) has chosen to use PATHEXT, but CMD is far from perfect here as well because it requires execute access in order to open a data file[1].


  1. The shell API should support a “run” operation akin to the already supported “runas” operation, and both should require that the user has the right to execute the file. Even without assistance from the shell API, the “Python.File” progid could define a “run” operation as the default instead of “open”, for which the interpreter could require the user to have the right to execute scripts and modules (e.g. via the use of a -X restrict_execution command-line option). Model behavior on Windows would be to require execute access for modules as well as scripts, just as execute access is required for DLL shared libraries (in contrast to POSIX). It’s not like most people would notice the change. Generally inherited permissions are so lax on Windows that execute permission is always granted along with read permission, if the user has any access at all. ↩︎

1 Like

Are you able to link us to any more information on Google Cloud CLI’s issues? (We’re familiar with the OS limitations you linked from our own docs, but those shouldn’t affect any single application - only multiple, distinct applications that are attempting to share private files.)

I remember the last time I tried using it with the Store version of Python (around July 2022), I had a problem with actually running the gcloud CLI. Since then, I’ve been using the version of Python installed with winget and haven’t had any problems. So based on your comment, I have now tried to install gcloud CLI and install some components into it with the Store version of Python and it seems to be working fine. So sorry for the noise, it looks like I can switch to the Store version once again :slight_smile:

Well, with Microsoft Store version I cannot run python scripts simply by writing their name. There is no way how to dynamically find the correct python executable, so when I got it working from normal command line, it does not work from venv (wrong python executable is started). Reverting back to the configuration which actually works - with Python Installer + Python Launcher and updated PATHEXT.

Okay, so it’s the lack of the launcher that’s an issue. Unfortunately there’s no good way to install it via the Store, but it might be time to make it easier to install the launcher on its own (it has no trouble launching Store installs), at least for people who have admin/install permissions on their machines.

Yes, it turns out that my current problem is actually missing the launcher functionality (i.e. finding the correct version of Python to run .\script.py, be it the one from the global installation or from venv). I just uninstalled the “normal” Python version and kept only the launcher installed and the Store version works as intended even from the venv environment (./script.py is executed fine with the PATHEXT update).

Another issue of the launcher functionality and/or Python installation (both the “normal” and Store one) is that python3 shebang (#!/usr/bin/env python3) does not work in venv environment - global Python environment is used during execution instead on the venv one. The reason is that the launcher does not find python3.exe executable in .\venv\Scripts and selects the global python.exe (workaround is to copy python.exe to python3.exe inside .\venv\Scripts\). Strange is that the venv environment contains pip.exe, pip3.exe, pip3.11.exe, but only python.exe and no python3.exe nor python3.11.exe.

pip decided to generate all the versioned shortcuts for things it creates. Python decided to only create the unversioned one for its own shortcuts. Not too surprising that different tools made different decisions.

That is fine, people make many decisions. As a result, Pip works, Python does not. :sweat_smile: