4

I am looking to permanently modify the path variable inside windows from a python script. The script is a wrapper to help automate the installation of applications and I want to enable the API to add applications to the path.

So for example I want to install a program called micro which has an install path of C:\Users\USERNAME\Path\to\micro and then add that install path to my path variable so that I can just run micro in my terminal.

I've been made aware of 2 possible solutions, which both do not work:

1. Using os.environ

In python the os module let's you read environment variables, but not actually modify them. so for example:

program_path = "C:\\Users\\USERNAME\\Path\\to\\micro"
new_path = f"{os.environ['PATH']};{program_path}"
os.environ["PATH"] = new_path

This would update the path variable in the python script, but it does not actually modify it on the system which is what I want.

2. setx

I was made aware that it is possible to update your path using the setx command in windows, but for some reason on windows 10 this destroys your path variable.

The idea is that you can call the setx command from python and use it to update the path variable. You should be able to type setx path "%path%;C:\Users\USERNAME\Path\to\micro" and have it update correctly.

So for example, in python code that would be:

program_path = "C:\\Users\\USERNAME\\Path\\to\\micro"
subprocess.Popen(f'setx path "%path%;{program_path}"')

This should take the current path variable and append the program path to it, but instead it just wipes your entire path and replaces it with a literal %path% and then the program path.

So now my path looks like this:

%path%
C:\Users\USERNAME\Path\to\micro

Any ideas on how to get this to work would be appreciated.

5
  • 1
    setx is a bad idea in that form, since you lose all environment variable references inside the result, which can easily lead to truncated PATH values (very vexing to debug). Note also that changing the value in the registry, what setx does, will not change anything for already-running processes, except for Explorer (which is why new applications launched after the change will pick up that change). So if you run Python with your script from your shell and expect things to change after Python exits, well, you won't like the result. Commented Sep 7, 2020 at 18:57
  • what does this have to do with powershell? [grin] Commented Sep 7, 2020 at 21:32
  • can you get the existing path into a Variable? if so, build the new full path and THEN use Setx.exe. ///// my pref would be to use the dotnet >>> [System.Environment]::GetEnvironmentVariable('path', 'machine') <<< and the matching Set method. to add to it. i don't know how to do that with python, tho. [blush] Commented Sep 7, 2020 at 21:35
  • 1
    setx.exe can be convenient if you don't want to be bothered writing the few lines of code it takes to broadcast a WM_SETTINGCHANGE "Environment" message to top-level windows, which is what makes Explorer reload its environment from the registry. However, the value that you modify and set should be the unexpanded entries from the system or user "Path" value in the registry, obtained with the winreg module. Do not use os.environ['PATH']. Commented Sep 7, 2020 at 21:51
  • 1
    The current user "Path" value is in "HKCU\Environment". The system "Path" value is in "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" (because the session manager, smss.exe, is the first process that needs it when it spawns csrss.exe, wininit.exe, and winlogon.exe for sessions 0 and 1). Commented Sep 7, 2020 at 21:58

1 Answer 1

4

Okay, so after a long (and disgusting) amount of research I found a solution. Here is the method I came up with for a cross-platform system of adding to PATH variable:

def add_to_path(program_path:str):
    """Takes in a path to a program and adds it to the system path"""
    if os.name == "nt": # Windows systems
        import winreg # Allows access to the windows registry
        import ctypes # Allows interface with low-level C API's

        with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root: # Get the current user registry
            with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as key: # Go to the environment key
                existing_path_value = winreg.EnumValue(key, 3)[1] # Grab the current path value
                new_path_value = existing_path_value + program_path + ";" # Takes the current path value and appends the new program path
                winreg.SetValueEx(key, "PATH", 0, winreg.REG_EXPAND_SZ, new_path_value) # Updated the path with the updated path

            # Tell other processes to update their environment
            HWND_BROADCAST = 0xFFFF
            WM_SETTINGCHANGE = 0x1A
            SMTO_ABORTIFHUNG = 0x0002
            result = ctypes.c_long()
            SendMessageTimeoutW = ctypes.windll.user32.SendMessageTimeoutW
            SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, u"Environment", SMTO_ABORTIFHUNG, 5000, ctypes.byref(result),) 
    else: # If system is *nix
        with open(f"{os.getenv('HOME')}/.bashrc", "a") as bash_file:  # Open bashrc file
            bash_file.write(f'\nexport PATH="{program_path}:$PATH"\n')  # Add program path to Path variable
        os.system(f". {os.getenv('HOME')}/.bashrc")  # Update bash source
    print(f"Added {program_path} to path, please restart shell for changes to take effect")

Neither are pretty, but it does actually work. You do need to restart running shells for it to take effect, but other than that it's perfect.

EDIT: Since this answer is getting more views I will add in a warning, if the changes fail to apply this will wipe your PATH. I have not yet found a solution to this besides wrapping the function in long validation code for each possible error, and actually had this happen to me in vendor code for a java tool (so I'm not the only one to encounter this issue). As per usual with the registry it's a bit finicky and a bit dangerous, good luck

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the code. It didn't quite work for me though. I had to change 2 things in the Windows part. It reads the value with the hard coded enum value of 3 but that did not return the path for me. It returned some other variable in the environment. I changed that to winreg.QueryValueEx(key, "PATH")[0]. Then there was a small change to put the ";" bit before program_path rather than after and all was good. Thanks

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.