The current working directory, or CWD for short, is a process-wide property. It is good practice to treat the CWD as read-only because it is essentially global state: if you change the CWD of your process at any point, any relative paths you might have stored in memory1 will stop working. I learned this first many years ago when using the Boost.Filesystem library: I could not find a function to change the CWD and that was very much intentional for this reason.
The good thing is that, in the vast majority of the cases, you do not have to change the CWD of your process. For example, when you spawn a subprocess, you can change the CWD of that subprocess only, not your own. So think twice before you call into
chdir: there is almost certainly another way to do what you intend without using
Unfortunately, there might still be cases where you are forced to change the CWD of your process. And in those cases, you must be prepared to restore the previous directory to minimize the scope of your global state changes. The common thing you will see people do is:
import os previous = os.getcwd() os.chdir("/bin") try: print("Entered", os.getcwd()) # Do some time-consuming operation. finally: os.chdir(previous) print("Restored", os.getcwd())
Which looks OK, right? Except… the file system is a system-wide multi-threaded beast. What happens if the previous directory moved or disappeared between the
os.getcwd call and the
os.chdir call? The second call will fail, you won’t be able to go back to the previous directory, and you will leave the process in an inconsistent state because there is no way for you to recover from the error.
To resolve this, think about file descriptors, not paths.
The idea is to open a file descriptor for the CWD before you change it. If this operation fails, you fail early before you get to do any state-mutating operations (the
chdir). And if this operation succeeds, then you hold a reference to the previous directory so, no matter what happens to the file system, you will be able to restore access to the previous CWD. Then, when you are done with your operations, you restore the CWD by using the
fchdir system call instead. The above code would look like:
import os previous = os.open(".", os.O_RDONLY) try: os.chdir("/bin") try: print("Entered", os.getcwd()) # Do some time-consuming operation. finally: os.fchdir(previous) print("Restored", os.getcwd()) finally: os.close(previous)
That’s it. This is a very simple change that will make your code more robust in the face of file system modifications.
You might want to try to see what happens if you do actually move or delete the previous directory while “Do some time-consuming operation” runs. Replace that line with a
time.sleep(10) and mess around with the previous directory from the console. Note that you might get errors coming from the
os.getcwd calls in log messages, but the
os.fchdir call will succeed.
There are plenty of programs around that turn any paths given to them to their absolute form to workaround issues with changes to the CWD. Doing so, however, carries its own problems: users typically provide relative paths to programs and if the program modifies them in any way, users get confused because what they typed doesn’t match what the program printed back in e.g. error messages. ↩︎