Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Python Change Directory: os.chdir, pathlib, and Why You Probably Should Not

Sean

Platform Writer

Aug 07, 2026
8 min read

Python changes the working directory with os.chdir(path), reads it back with os.getcwd(), and pathlib has no equivalent because changing directories is process state rather than a path operation.

Python Change Directory: os.chdir, pathlib, and Why You Probably Should Not

That omission is deliberate, and it is a hint. os.chdir mutates something global to the entire process — every thread, every library, every relative path opened afterwards. Most code that reaches for it wants absolute paths instead and has not realised it yet.

Table of contents

The basic operation

import os

print(os.getcwd())        # /home/app
os.chdir("/var/data")
print(os.getcwd())        # /var/data

os.chdir accepts a string, a Path, or anything implementing the filesystem path protocol, so pathlib objects work directly:

from pathlib import Path
import os

os.chdir(Path.home() / "projects" / "api")

It raises FileNotFoundError if the directory does not exist, NotADirectoryError if the path is a file, and PermissionError if you cannot enter it. All three are subclasses of OSError, so catching that covers the set — though catching the specific one usually produces a better error message.

Why pathlib does not have chdir

pathlib models paths as values. A Path is an immutable object describing a location; methods on it return new objects or touch the filesystem at that location. The current working directory is not a property of any path — it is a property of the process.

So pathlib gives you Path.cwd() to read it and stops there. To change it you go through os.chdir, passing a Path if you like.

from pathlib import Path
import os

here = Path.cwd()
os.chdir(here.parent)
print(Path.cwd())

This is not an oversight to work around. It is the library telling you that directory changes are a different category of operation from path manipulation, and the distinction is worth respecting.

The problem with process-global state

There is exactly one working directory per process. Not per thread, not per module, not per function. When you call os.chdir inside a helper, you have changed it for every other piece of code running in that process.

def load_config():
    os.chdir("/etc/myapp")        # helpful, briefly
    with open("config.yaml") as fh:
        return yaml.safe_load(fh)

config = load_config()
open("output.log", "w")           # writes to /etc/myapp/output.log

The function did its job and left a landmine. Every relative path used afterwards resolves somewhere unexpected, and the failure surfaces far away from the cause — usually as a file appearing in a directory nobody expected, or a FileNotFoundError for a file that plainly exists.

In a threaded application it is worse, because there is no ordering guarantee. Thread A chdirs, thread B opens a relative path, and whether B gets the right file depends on timing. That is a bug that reproduces once a week and never under a debugger.

This is also why it breaks in async code: await hands control to another coroutine that may be mid-way through its own relative path work.

The context manager, when you genuinely need it

Sometimes you have no choice. A subprocess must run in a specific directory, or a library resolves relative paths internally and gives you no way to configure a base. In that case, contain it.

import os
from contextlib import contextmanager
from pathlib import Path

@contextmanager
def working_dir(path):
    previous = Path.cwd()
    os.chdir(path)
    try:
        yield Path(path)
    finally:
        os.chdir(previous)
with working_dir("/var/data"):
    run_legacy_tool()          # insists on relative paths
# back wherever we were, even if run_legacy_tool raised

The try/finally is the part that matters. Without it, an exception leaves the process in the wrong directory and the next unrelated operation fails for reasons that make no sense.

Python 3.11 ships contextlib.chdir, which is the same idea in the standard library:

from contextlib import chdir

with chdir("/var/data"):
    run_legacy_tool()

Neither version is thread-safe, and neither can be. The state being restored is global; a second thread changing it concurrently will produce whatever it produces. If you need per-task directories, use processes, not threads.

What to do instead

Nearly every use of os.chdir is a workaround for building paths relative to a known location. Build the paths instead.

from pathlib import Path

BASE = Path(__file__).resolve().parent

config = BASE / "config" / "settings.yaml"
output = BASE / "out" / "report.csv"

with config.open() as fh:
    ...
output.parent.mkdir(parents=True, exist_ok=True)

Path(__file__).resolve().parent is the directory containing the module, resolved through any symlinks. It does not care what the working directory is, which means the code behaves identically whether it was launched from the project root, from /, or by a scheduler with no meaningful cwd at all.

For subprocesses, pass the directory instead of changing your own:

import subprocess

subprocess.run(["npm", "run", "build"], cwd="/srv/frontend", check=True)

The cwd argument sets the working directory for the child process only. Your process never moves. This is the correct answer roughly every time someone reaches for chdir before a subprocess call.

Where this bites in deployment

Code that depends on the working directory works on a laptop, where you cd into the project and run python app.py, and then behaves differently everywhere else. A container sets WORKDIR to whatever the image says. A systemd unit starts in /. A cron job starts in the user’s home directory. A process manager may start in the directory it was itself launched from.

The symptom is the classic one: works locally, FileNotFoundError: config.yaml in production, and the file is definitely in the image.

Services deployed on RunxBuild run from the repository root, so relative paths from there behave predictably — but relying on that is still relying on a convention. Anchoring to Path(__file__).parent and reading configuration from environment variables rather than files found relative to the cwd removes the class of problem entirely, and it makes the same code run identically on your laptop, in CI, and on the platform.

How this fits the rest of the stack

Path handling is a small correctness question sitting inside a larger cost question. The runtime, the storage the files live on, the database, and the bandwidth all carry their own numbers, and the total is what arrives monthly. The RunxBuild hosting calculator lays those line items out together so you can model them before committing.

Useful related references:

FAQ

How do I change the current directory in Python?

Use os.chdir(path) and read it back with os.getcwd(). It accepts strings and pathlib Path objects. It raises FileNotFoundError, NotADirectoryError, or PermissionError depending on why the change failed, all subclasses of OSError.

Does pathlib have a chdir method?

No. Path.cwd() reads the working directory but there is no method to change it, because pathlib models paths as immutable values while the working directory is process state. Pass a Path to os.chdir when you need to change it.

Is os.chdir thread-safe?

No. There is one working directory per process, shared by every thread. If one thread changes it while another resolves a relative path, the result depends on timing. The same problem affects async code, since an await can hand control to a coroutine mid-way through its own path work.

What is the safest way to temporarily change directory?

A context manager that restores the previous directory in a finally block, so an exception cannot leave the process somewhere unexpected. Python 3.11 and later ship contextlib.chdir which does exactly this. Neither form is thread-safe.

How do I avoid needing chdir at all?

Build absolute paths from a known anchor: BASE = Path(file).resolve().parent, then BASE / config / settings.yaml. For subprocesses, pass cwd= to subprocess.run so the child gets the directory and your own process never moves.

#python cd#os.chdir#python working directory#pathlib#python file paths