Repository navigation
Description
Install source and version
- Installed from the Windows Store
- Installed with the MSIX from python.org
- Installed with the MSI from python.org
- Installed with
winget install 9NQ7512CXL7T
Version: 26.2
Describe the bug
I encountered the bug while trying to use vermin (package) through the executable aliases that pymanager generates in %USERPROFILE%\AppData\Local\Python\bin. However i believe this is a much broader problem related to the use of multiprocessing module.
When multiprocessing is used with the spawn and forkserver start methods, child processes must be able to safely import the main module without executing the same threading logic.
For example, vermin doesn't execute its main function, it is called explicitly by an executable, making the main module (vermin.main) safe to import (as far as i understand). However, from my experimentation, the executable alias that pymanager provides (vermin.exe) calls vermin.exe.__script__.py, which isn't safe to import as it calls vermin.main() unconditionally.
I expect executable aliases to fail in a similar manner with other packages that rely on multiprocessing module the same way. But I wasn't able to confirm it yet.
I can think of a few ways to fix this issue:
- Calling the original executable directly if it exists in
%USERPROFILE%\AppData\Local\Python\pythoncore-*\Scripts
Probably the most reliable, but not very versatile. - Making
*.__script__.pyfiles safe to import by adding an explicit__name__check:if __name__ == '__main__': from ... import main sys.exit(main())
To Reproduce
Steps to reproduce the behavior (an example using vermin):
- Install vermin 1.8.0
py -m pip install vermin==1.8.0 - Refresh executable aliases in
%USERPROFILE%\AppData\Local\Python\binpy install --refresh
- Ensure
%USERPROFILE%\AppData\Local\Python\binis in thePATHand%USERPROFILE%\AppData\Local\Python\pythoncore-*\Scriptsis not (or has lower priority). Alternatively you can use%USERPROFILE%\AppData\Local\Python\bin\verminin step 5 to emulate the same conditions.where vermin - Create an empty directory with a single empty
main.pyfile. - From this directory run
vermin --processes=2 . - See vermin infinitely calling itself and displaying errors
RuntimeError: If running `detect_paths()` outside of `if __name__ == "__main__":` it, or the code calling it, must be done within it instead. No files specified to analyze!
Expected behavior
If you run vermin with --processes=1 (it avoids using multithreading in that case), or if you run the original vermin executable in %USERPROFILE%\AppData\Local\Python\pythoncore-*\Scripts instead, it will display the expected report without any errors:
Tips:
- Since '# novm' or '# novermin' weren't used, a speedup can be achieved using: --no-parse-comments
(disable using: --no-tips)
Minimum required versions: ~2, ~3
Note: Not enough evidence to conclude it won't work with Python 2 or 3.
Additional context
vermin.exe.__script__.py, generated consistently with py install --refresh.
Activity
zooba commentedon Jun 5, 2026
I suspect something else is at play here, since
vermin.exe.__script__.pyisn't importable and shouldn't appear insys.argv, but possiblymultiprocessinghas ways to find its way back to it.Could you try adding
if __name__ == '__main__':over the entire script file and see if that works? (It's fine to edit the script in place and then run the executable to launch it.) We shouldn't need to do anything at all if we're not actually launching it ourselves, and excluding the entire script is safest. I worry that it'll still miss some initialization that's needed for the app, though, so rather than debugging that on the next release it'd be good to flush out those problems now.n67-exe commentedon Jun 6, 2026
That works for vermin. In this case it is actually identical to what i suggested since vermin never uses
sys.argv[0]apart from printing help which is always done in the main thread.There is probably no difference since
sys.argvin the child processes seems to be preserved from the main thread according to my experiments.I'm not sure i understand what you meant by that.
All Windows systems use
spawnstart method by default (vermin doesn't change that). It involves child processes initiating a new python interpreter and then importing the__main__module.A quick peek into implementation of multiprocessing shows that it relies on
sys.modules['__main__']:I added logging to both
vermin.exe.__script__.pyandvermin\main.py(to the beginning of the file) to see when they are imported:Running
vermin --processes=1 .(through the alias in%USERPROFILE%\AppData\Local\Python\binwithout any modifications) i get:Output
This is followed by vermin output. As expected each module is imported once, in order.
Running the broken
vermin --processes=2 .(through the same alias) i get:Output
The last 4 entries repeat infinitely with different process ids interleaved with each other and errors. This shows that
vermin.exe.__script__.pyis imported from the child threads as well. Since it callsvermin.main()when imported this leads to child processes creating their own child processes and so on.With the added
if __name__ == '__main__':tovermin.exe.__script__.pyi get the following output:Output
This is again followed by vermin output. There is no difference in the output if i add
if __name__ == '__main__':over the entire script instead of just the main call. The thread pool is created twice in the vermin implementation, so we see 2 pairs of threads launched one after another. Depending on the timing and the amount of work available, one of the threads might not perform any work (like the thread129492in this run).zooba commentedon Jun 8, 2026
Ah, I see. We're not updating the module's
__spec__.origin(aka__file__), and that's what is being used to get back to the script file.To be clear, this is entirely
multiprocessing's doing and not Vermin - I expect any genericmultiprocessingscript would behave the same (I just haven't had time to build one to test it).Unfortunately, I expect that running a
__main__that does nothing is also likely to breakmultiprocessing-based apps that expect it to do something. There's not really much we can do about that.Another snippet that might work without the
__name__ == '__main__'check is this one (anywhere in the file before the main call). Could you try putting that at the top of the script file, remove the other check we added, and see if that also works?(I prefer this one because nobody should ever import these script files, and the sooner they break because they are trying, the easier it is to figure out what they're getting wrong. If it looks like it works, the eventual error could be far removed from the actual mistake, which means it's much harder to debug the issue.)
n67-exe commentedon Jun 10, 2026
Your suggested approach works with vermin and it prevents the script from being imported at all in child threads.
With the following
vermin.exe.__script__.py(added more logging before and after checks):Script
I get this output for
vermin --processes=1 .:Output
I get this output for
vermin --processes=2 .:Output
The difference compared to the
if __name__ == '__main__':fix is that child threads never importvermin.exe.__script__.py, and as a side effect module__mp_main__never appears in the log. While__mp_main__is a valid entry insys.modulesfor child threads, in this case it has the name__main__(it always equalsmaininvermin.mainmodule). I have no idea if this is a problem and if any package relies on__mp_main__in some way.Another point to consider is that several launcher scripts generated by other libraries (distlib, pip, uv) use a simple
if __name__ == '__main__':check.My personal opinion is that it would be reasonable to be consistent with other implementations (to avoid any "pymanger specific" problems). That or just relying on already existing executables generated in
%USERPROFILE%\AppData\Local\Python\pythoncore-*\Scriptsas i suggested earlier.zooba commentedon Jun 11, 2026
This has some validity, though it is strictly more correct for the script to not be loaded at all. Anything relying on
__mp_main__is almost certainly making an invalid assumption, and it's definitely reliant on private/undocumented details. I think I'll go with both.Thanks for the help testing.
That's fine if you want to add it to your
PATHinstead of the PyManager directory. But we aren't able to manage those for you, and can't add those directories for you, so you're on your own.Our official recommendation is to create a virtual environment and activate it to get the correct scripts on PATH.
n67-exe commentedon Jun 12, 2026
What i meant is that inside
%USERPROFILE%\AppData\Local\Python\bina generated executable alias (.exe) can use the target interpreter path (from.exe.__target__) to locate%USERPROFILE%\AppData\Local\Python\pythoncore-*\Scriptsdirectory and launch an executable directly from there. This way it bypasses a python launcher script (.exe.__script__.py) and thus avoids the problem of maintaining such script entirely on our part.This of course assumes that for every alias pymanager creates there already exists an executable in
%USERPROFILE%\AppData\Local\Python\pythoncore-*\Scriptswhich sounds like it's always true to me but i don't know for sure.zooba commentedon Jun 12, 2026
There are two totally independent tools involved, so not necessarily, though it's reasonable that if someone has tried to disable pip from generating their aliases (which is hard), they probably don't care to have PyManager's aliases either (though disabling them is easy). So in practice, most of the time pip will generate aliases. But with literal millions of users, even 99% isn't enough to stop thousands of people being broken. So we can't rely on it.
Plus there's the performance cost of multiple executables being involved. Our aliases are written specifically to avoid too many processes being created, but distlib's aren't (and probably can't be), so launching a PyManager alias for a recent CPython should be noticeably faster (and will be more reliable if you're trying to wait for it to complete).
The tradeoff is there may be slightly different behaviour, at least until we smooth out the bumps (the PyManager support has been there less than a year for aliases), but it's worth it for the better/safer/faster result.
Make entrypoint script less noticeable. (python#365)