Relative Paths in Python: Terminal, IDE and Service Launches
How Python resolves relative paths depends on the process working directory, which varies by launcher. This guide shows how to inspect the working directory, choose and document an explicit path base, and understand why __file__ cannot always be trusted.
On this page
The short answer
A relative path such as Path("data/config.json") is resolved against the current working directory of the process, not the location of your script. The working directory is whatever os.getcwd() reports when the interpreter starts, and terminal, IDE, and service launchers commonly set it differently. Print Path.cwd() at startup to see the real value. Then choose an explicit base deliberately: the working directory for user-supplied files, __file__'s parent for bundled resources, or a configured absolute path for services. Because __file__ is an optional attribute and can be unset for some modules, guard it with getattr. pathlib's absolute() and resolve() differ: resolve() also eliminates '..' and follows symlinks. There is no universal default that works everywhere; verify each launch environment and log both the working directory and the resolved path.
Why the working directory governs relative paths
A path like data/config.json is not interpreted by Python itself; it is passed to the operating system, which resolves it against the process's current working directory. That directory is set once when the process starts and does not follow your script. pathlib.Path.cwd() returns a new path object representing the current directory, as returned by os.getcwd(). The same directory can therefore yield one result in a terminal, another in an IDE, and a third under a service manager. The first step in diagnosing a missing-file error is never guessing at the script's location, but recording what the process actually sees as its directory.
import os
from pathlib import Path
# 1. Inspect what the launcher really set.
print("cwd:", os.getcwd())
print("cwd path:", Path.cwd())
print("script dir:", Path(__file__).resolve().parent)Choosing an explicit base for each launch context
Once you know the working directory, decide deliberately which base you want. For user-supplied inputs, the working directory is often the right choice because the user expects paths to be relative to where they launched the tool. For bundled resources that travel with your code, base the path on the script or package location instead. For long-running services, prefer a configured absolute path so the service behaves the same regardless of how the process manager starts it. Document the chosen convention near the code that opens the file, because future maintainers will otherwise assume the script location is the default.
Using __file__ carefully when it is available
The module __file__ attribute can point to the file that defined the current code, which makes it useful for locating bundled resources. The data model documentation warns that __file__ is optional and may be unset for some modules, including statically linked C modules or modules loaded from unusual sources. Guard access with getattr so the code does not raise when the attribute is missing. When __file__ is present, resolve it to an absolute form before deriving sibling paths, because a relative __file__ would still depend on the working directory.
absolute() versus resolve() in pathlib
pathlib offers two ways to make a path absolute, and they are not interchangeable. Path.absolute() makes the path absolute without normalization or resolving symlinks, which is closer to os.path.abspath() but still preserves '..' segments for safety. Path.resolve() makes the path absolute, eliminates '..' components, and follows symlinks, which is closer to os.path.realpath(). If you need the real location of an existing file, resolve() is usually the better choice. If you only need a stable absolute form without touching the filesystem, absolute() may be preferable.
Logging the working directory and the resolved path
When a file is not found, log both the working directory and the exact path you attempted to open. That pair usually explains the failure faster than guessing about the script location. Include the launcher context when possible, such as whether the process was started from a terminal, an IDE run configuration, or a service manager. If the path depends on configuration, log the configuration value as well. These records make it possible to reproduce the environment later instead of assuming a universal default.
Common launcher differences to verify
Terminal launches often start with the shell's current directory, which may be the project root or the folder you cd'd into. IDE run configurations can set the working directory to the project root, the module folder, or a custom value defined in the launch settings. Service managers and container entrypoints may start the process in a system directory or a declared working directory that differs from the code location. Because these defaults vary, test the actual startup directory in each environment rather than relying on one machine's behavior.
A practical startup check for path-sensitive code
For applications that open files early, add a small startup check that prints or logs the working directory and the base path you intend to use. If the base comes from __file__, verify that the attribute exists and resolve it before use. If the base comes from configuration, confirm that the configured path is absolute or document how it will be interpreted. This check is especially useful in automated environments where the launch directory is not obvious from the source code.
Limitations and version considerations
The behavior described here depends on the process working directory and on whether __file__ is set, both of which can vary across Python implementations and launch environments. pathlib's normalization can also change how a path is interpreted by other tools, so pathlib is not a complete drop-in replacement for os.path in every scenario. Some pathlib methods changed across recent Python versions, including stricter handling of symlink loops and reserved paths, so verify behavior on the target runtime. When portability matters, prefer explicit absolute bases and avoid assuming that relative paths will resolve the same way everywhere.
Things to check
- Print Path.cwd() or os.getcwd() at startup to confirm the actual working directory for each launcher.
- Use an explicit base for relative paths: working directory for user files, __file__'s parent for bundled resources, or a configured absolute path for services.
- Guard __file__ with getattr because it is optional and may be unset for some modules.
- Choose pathlib's resolve() when you need '..' eliminated and symlinks followed, and absolute() when you only need an absolute form without normalization.
Where this applies
Relative path resolution depends on the process working directory, which differs across terminal, IDE, and service launches. __file__ is optional and may be missing for some modules. pathlib's absolute() and resolve() behave differently, and pathlib's normalization can change how paths are interpreted by other tools. Some pathlib behaviors also changed across recent Python versions, so verify on the target runtime.