There are two questions being talked about on this forum that look interchangeble but have very different answers.
The first question...
...which is what the OP seems to want but doesn't quite specify, is about finding external files that are not part of the application, are intended for the user to see and interact with and possibly modify or provide themselves. In onefile mode, the external files will still be visble. In PyInstaller >=v6 onedir mode, the files will be outside of the _internal directory. And for macOS .app bundles, this won't make any sense at all.
For this, the closest answer would be the following (but please read on before splodge+pasting):
if getattr(sys, "frozen", False):
app_location = os.path.dirname(sys.executable)
else:
app_location = os.path.dirname(sys.modules["__main__"].__file__)
# Just os.path.dirname(__file__) is OK if this is the top level script
For the curious, the above is mostly equivalent to using os.path.dirname(sys.argv[0]) except:
- If the executable's location is added to
PATH then invoked by name then sys.argv[0] would just the exectuable's name rather than its location.
- If the executable is invoked via a symlink,
sys.argv[0] will point to the symlink but sys.executable will point to the resolved file.
A word of caution though: It is normal for security concious users to install applications into readonly locations which is obviously incompatible with the idea of having a writable config file next to the application. Additionally, what exactly this location means get ambiguous in say a macOS .app bundle or an unfrozen Python application invoked via python -m package_name or a console script entry point. To that end, I encourage instead putting configuration in the user's home directory, even though it breaks the nice asthetics of being self contained (possibly with an opt-in portable mode if you prefer).
The second question...
... which wasn't strictly asked but is an understandable interpretation of application path and is what most people have answered, is about locating application resources shipped inside the application. These are files that user shouldn't be touching and, in onefile mode, won't even be able to see. I want to address this one since I see these invalid (to both questions) answers being proliferated all across the internet. Every other answer on here addressing that question is broken (with the exception of sys._MEIPASS which is merely unnecessary).
The correct method is to use __file__ in exactly the same way as you would if you weren't using PyInstaller. You shouldn't need any if getattr(sys, "frozen", False) forks anywhere.
# These will work regardless of whether or not you're using PyInstaller
resource = pathlib.Path(__file__).resolve().with_name("resource.txt")
# Or
resource = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resource.txt")
If that ends up looking in the wrong place then that means that the dest part of your --add-data options needs adjusting so that your frozen application layout is the same as your normal application layout. Always adjust your frozen application so that it's consistent with your unfrozen layout and your Python code – never do it backwards and adjust your Python code to compensate for an inconsistent packaging layout.
Why you should never use the other answers (even if you're not using PyInstaller):
- Anything involving
sys.path[0] is just a wild heuristic that sometimes happens to work because Python usually prepends your script's location to sys.path. Just running python -I your-code.py is enough to break that assumption. This will also break if you make your code into a pip installable package so that it moves to a different entry in sys.path.
- Using
sys.executable will break in onefile mode or with macOS .app bundles since the application contents are not next to the executable. It also breaks in onedir mode in PyInstaller v6 since the application resources are moved into a subdirectory unless you specify --contents-directory=. (an option that only exists because people made such a fuss about refusing to fix their invalid os.path.dirname(sys.executable) based resource locating).
- Using
sys.argv[0] has all the issues of sys.executable but will aditionally break if the application is launched via a symlink.
- Using
os.cwd() is equivalent to and no less broken than just using a relative path.. It assumes that your code is in your current working directory. python test.py will give the illusion of working but python somewhere/else/test.py will break.
- Using
os.chdir() affects the global process state so if two libraries try to do this, the one that loads second will break the one that loads first. It'll also break legitimate usages of relative paths such as those passed in as command line arguments to a console application.