How to create a directory and its missing parents in Python

Creating a directory is one call. Creating one several levels deep, without failing when it already exists and without a race condition, needs two keyword arguments that are easy to forget.

The answer

from pathlib import Path

Path("/tmp/a/b/c").mkdir(parents=True, exist_ok=True)
exists : True
is_dir : True

Both keywords earn their place. Without parents=True a missing intermediate directory is an error:

mkdir() with a missing parent -> FileNotFoundError: [Errno 2] No such file or directory: '/tmp/demo0049/x/y'

And without exist_ok=True an existing directory is an error:

mkdir(parents=True) on an existing dir -> FileExistsError: [Errno 17] File exists: '/tmp/demo0049/a/b/c'

The os equivalent is os.makedirs(path, exist_ok=True), where makedirs already implies parents=True. Both are fine; pathlib reads better once you are also joining paths and reading files.

Why not to check first

The version you will write without thinking about it:

if not path.exists():
    path.mkdir()

Between the exists() and the mkdir(), another process can create the directory — and then your mkdir() raises the very error you were trying to avoid. It is a check-then-act race, and it will fail rarely enough to be puzzling.

exist_ok=True pushes the whole thing into one system call, where the kernel settles it. Same idea as try/except over if exists: do the thing and handle the failure, rather than asking a question whose answer expires immediately.

exist_ok=True does not mean “if anything is there”

Worth knowing, because the name suggests otherwise. Put a file at the path:

a FILE at the path, mkdir(exist_ok=True) -> FileExistsError: [Errno 17] File exists: '/tmp/demo0049/clash'

exist_ok=True tolerates an existing directory. Anything else at that path is still an error — which is the right call, and worth knowing if you are tempted to treat it as “make sure this path is usable”.

Permissions, and the two surprises

mkdir() takes a mode, and the value you pass is not the value you get:

mkdir(mode=0o700) -> drwx------
mkdir(mode=0o777) -> drwxr-xr-x
current umask     : 0o22

The mode is masked by the process umask, so 0o777 with the usual 0o022 umask produces 0o755. This is standard Unix behaviour rather than a Python quirk, but it catches people who set a mode and then check it.

The second surprise is more specific:

with parents=True, the PARENT gets the default mode, not yours:
  parent drwxr-xr-x  inner drwx------

Created in one call with mode=0o700, parents=True, only the leaf got the mode. The parents were created with the default. If you are building a tree that is meant to be private, the intermediate directories are not — and nothing warns you.

Set the modes explicitly afterwards if it matters, with Path.chmod.

The case you actually have

Nine times in ten, you are about to write a file and its directory may not exist:

outfile = Path("reports") / "2026" / "out.txt"
outfile.parent.mkdir(parents=True, exist_ok=True)
outfile.write_text("done\n")

.parent is the whole trick — you have the file path, and you need the directory containing it.

When you want it gone again

If the directory is temporary, let Python handle it:

with tempfile.TemporaryDirectory() as tmp:
    ...
inside the with block : True
after the with block  : False

tempfile.TemporaryDirectory removes the tree on exit, including on an exception. Much better than a hand-rolled path in /tmp that survives every crash.

Removing by hand has one asymmetry worth remembering:

Path.rmdir() only removes an EMPTY directory:
  -> OSError: [Errno 39] Directory not empty

rmdir() refuses a non-empty directory. shutil.rmtree removes the whole tree without asking, which is exactly as dangerous as it sounds.

About Netcup (advertisement)

The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).