Changing the working directory looks simple, but it affects the entire process. A call to os.chdir() changes the base used by relative paths, file loading, external commands, and many libraries. contextlib.chdir() makes that change temporary and restores the previous directory when the block exits.
This guide explains how to use contextlib.chdir, why it is unsafe for threads and concurrent tasks, how exceptions and nested blocks behave, and when absolute paths or subprocess cwd are better choices.
Basic usage
from contextlib import chdir
with chdir("project"):
print(open("config.toml").read())
Inside the block, relative paths start from project. On exit, even after an exception, the previous directory is restored.
Why the context manager matters
import os
previous = os.getcwd()
try:
os.chdir("project")
run()
finally:
os.chdir(previous)
contextlib.chdir packages this pattern and reduces the risk of forgetting restoration after early returns or failures.
Process-wide state
The working directory does not belong only to the current function. It is shared by the process. While the block is active, unrelated code may resolve relative paths against an unexpected base. Keep the scope short and controlled.
Avoid it in concurrent code
Threads, callbacks, servers, and asynchronous tasks can run at the same time. A temporary directory change may break another flow. In concurrent applications, prefer absolute paths and pass cwd explicitly to subprocesses.
Prefer pathlib for file access
from pathlib import Path
base = Path("project").resolve()
config = (base / "config.toml").read_text(encoding="utf-8")
This avoids global state and is normally easier to reason about. The internal Python pathlib guide covers object-oriented path handling.
Subprocess working directories
import subprocess
subprocess.run(["python", "build.py"], cwd="project", check=True)
When only a child command needs another directory, cwd is safer than changing the whole process.
Exceptions and restoration
from contextlib import chdir
try:
with chdir("temporary"):
raise RuntimeError("failure")
except RuntimeError:
pass
The original directory becomes current again. Restoration can still fail if that directory was removed or renamed unexpectedly.
Nested blocks
with chdir("root"):
with chdir("subfolder"):
run()
Each block stores and restores its own previous directory. Resolve paths first when relative interpretation could be confusing.
Testing legacy tools
Tests can combine TemporaryDirectory and chdir for command-line tools that still depend on the current directory.
from contextlib import chdir
from tempfile import TemporaryDirectory
with TemporaryDirectory() as folder:
with chdir(folder):
create_test_files()
Public library APIs
Library functions should not silently change the working directory. Callers may not expect the global side effect. A cleaner API accepts a base directory or complete paths.
Security considerations
Validate user-provided directories. Resolve the path, restrict it to an approved root, and avoid running commands in attacker-controlled folders. The working directory may influence imports, configuration files, and executables discovered by external tools.
Common mistakes
- Using
chdirin a multithreaded server. - Keeping the context active during long operations.
- Assuming relative paths still refer to the same files.
- Changing directories for subprocesses instead of using
cwd. - Trusting an external directory without validation.
Recommended practice
Keep the scope small, avoid yielding control inside the block in concurrent programs, prefer absolute paths, and reserve chdir for sequential scripts, migrations, and controlled tests. For more complex resource lifecycles, see the internal contextlib resource-management guide.
Conclusion
contextlib.chdir provides reliable restoration, but it does not turn process-wide state into local state. It is convenient in linear scripts and tests; concurrent applications should use pathlib, explicit paths, and subprocess cwd.







