Skip to content

🐛 fix(batch): remember an undefined PATH so deactivate can restore it - #3493

Merged
gaborbernat merged 3 commits into
pypa:mainfrom
darrenhuai:fix/batch-unset-path
Oct 30, 2026
Merged

gaborbernat merged 3 commits into
pypa:mainfrom
darrenhuai:fix/batch-unset-path

Conversation

@darrenhuai

Copy link
Copy Markdown
Contributor

If PATH is undefined when you activate, deactivate.bat leaves the environment's Scripts folder on PATH, and a second activate.bat adds it twice. A new cmd window always has PATH defined, so you only hit this after something runs set PATH=, such as a build script that starts from a clean environment and builds its own PATH. With main (861cff7):

@echo off
set PATH=
call venv\Scripts\activate.bat
echo active: [%PATH%]
call venv\Scripts\activate.bat
echo again:  [%PATH%]
call venv\Scripts\deactivate.bat
echo after:  [%PATH%]
active: [C:\work\venv\Scripts;]
again:  [C:\work\venv\Scripts;C:\work\venv\Scripts;]
after:  [C:\work\venv\Scripts;]

cmd has no empty variables, so when PATH is undefined, @set "_OLD_VIRTUAL_PATH=%PATH%" in activate.bat deletes _OLD_VIRTUAL_PATH instead of saving an empty value. deactivate.bat only touches PATH when _OLD_VIRTUAL_PATH is defined, so it skips the restore. The re-activation guard from #3245 checks the same variable, so the second activate.bat doesn't deactivate first: it saves the active PATH as the one to restore and puts Scripts in front again. PKG_CONFIG_PATH, which that guard also protects, gets a second lib\pkgconfig entry the same way and is still set after deactivate. This is the batch half of the empty PATH case that #3259 fixed for bash and fish.

activate.bat now sets _OLD_VIRTUAL_PATH_UNSET=1 when PATH is undefined, in the same goto branch that saves _OLD_VIRTUAL_PATH, and the guard at the top also runs deactivate.bat when that marker is defined. deactivate.bat undefines PATH when it finds the marker, then clears it. Each new command is one line with no parenthesized block, so parentheses in PATH are still safe. Nothing changes when PATH is defined. With this branch the script above prints [C:\work\venv\Scripts;], [C:\work\venv\Scripts;] and [].

test_batch_path_round_trip now also starts from an undefined PATH, in normal and delayed expansion, for each of the existing environment names and prompt cases. It expects <Scripts>; after both activations and no PATH after deactivate. The script then sets PATH=%SystemRoot% and activates once more, to check that deactivate left nothing behind; a stale marker would make that activation drop the caller's PATH. The new path-unset cases fail with main's scripts.

Thanks for contributing, make sure you address all the checklists (for details on how see development documentation)

  • ran the linter to address style issues (tox -e fix). I ran the same hooks through prek on the changed files; the semgrep hook only looks at activate.sh.

  • wrote descriptive pull request text

  • ensured there are test(s) validating the fix

  • added a docs/changelog/+short-description.<type>.rst fragment for each user-facing change, or applied
    skip news

  • updated/extended the documentation (the docs already say deactivate restores the original PATH)

  • for changes to activation scripts, pyvenv.cfg, wheel downloads, app-data or release workflows: the outside input is the caller's PATH. The new lines only test whether PATH and the marker are defined, set the marker to the literal 1 and clear PATH. They never expand PATH, so its contents can't run as code, and the existing quoted set "_OLD_VIRTUAL_PATH=%PATH%" save is unchanged.

  • ran tests on Python 3.9 and 3.15 (tox run -e 3.9,3.15). I ran 3.14 locally and left 3.9 and 3.15 to CI.

  • linked upstream issues beside any temporary interpreter overrides (none)

  • kept the PR in draft until CI passes

When PATH is undefined at activation, for example in a build script that runs `set PATH=` first, activate.bat has
nothing to save. `set "_OLD_VIRTUAL_PATH=%PATH%"` expands to an empty value, which cmd treats as deleting the
variable, and deactivate.bat only restores PATH when _OLD_VIRTUAL_PATH is defined, so it left the environment's
Scripts folder on PATH. The re-activation guard from pypa#3245 checks the same variable, so a second activate.bat did not
deactivate first. It saved the active PATH as the one to restore and added Scripts a second time, and PKG_CONFIG_PATH
got a second entry the same way.

activate.bat now sets _OLD_VIRTUAL_PATH_UNSET when PATH is undefined, and the guard deactivates when that marker is
set too. deactivate.bat undefines PATH when it finds the marker and then clears it.
@darrenhuai
darrenhuai marked this pull request as ready for review October 30, 2026 01:47
Taking PATH from PATH itself or from an undefined variable leaves it as is or
undefines it, so the expected values read os.environ without a branch.
@gaborbernat
gaborbernat merged commit 9015069 into pypa:main Oct 30, 2026
77 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants