The Unix mkdir -p command is one of those small conveniences nobody appreciates until they don't have it. It creates intermediate directories on the way to the target, and it doesn't error out if the target already exists. The Python equivalent is one line: Path('path/to/dir').mkdir(parents=True, exist_ok=True). This is the highest-voted directory-creation question on Stack Overflow (over 5,800 upvotes), and the answer has settled cleanly since pathlib became standard in Python 3.4 via PEP 428. This entry covers what each flag actually does, the parameter truth table for every combination, the legacy os.makedirs() alternative, the file-in-the-way trap, and the race-condition caveats. Part of our Stdlib Essentials guide.

Key Takeaways

  • Canonical one-liner: Path('p').mkdir(parents=True, exist_ok=True) creates the full tree and is idempotent.
  • parents=True creates any missing intermediate directories (the -p behavior).
  • exist_ok=True silently succeeds when the target already exists as a directory.
  • File-in-the-way trap: if a regular file sits at the target path, FileExistsError fires regardless of exist_ok.
  • Legacy equivalent: os.makedirs(path, exist_ok=True) for code that predates pathlib.
Parameter truth table for Path.mkdir() across all four flag combinations parents + exist_ok: what each combination does FROM 5 LINES TO 1 LINE · TRUTH TABLE FOR EVERY SCENARIO LEGACY (5+ lines) import os try: os.makedirs("logs/today") except FileExistsError: pass cluttered, exception-driven MODERN (1 line) from pathlib import Path Path("logs/today").mkdir( parents=True, exist_ok=True) idempotent, declarative, race-safe PARAMETER TRUTH TABLE Scenario mkdir() default mkdir( parents=True) mkdir( exist_ok=True) mkdir(parents=True, exist_ok=True) Happy path target missing, parents all exist ✓ success ✓ success ✓ success ✓ success Missing parents target missing, intermediates missing FileNotFound Error ✓ success FileNotFound Error ✓ success Already a directory target exists as a directory FileExists Error FileExists Error ✓ success ✓ success File in the way target path occupied by a regular file FileExists Error FileExists Error FileExists Error FileExists Error KEY INSIGHT The far-right column handles 3 of 4 cases; the 4th (file in the way) is a real error that you should NOT silence. Use parents=True, exist_ok=True by default; let FileExistsError surface when a file blocks the path.
Top: the legacy 5-line pattern vs the modern 1-line equivalent. Bottom: what every parameter combination returns in each scenario.

The Canonical 2026 Answer

One line creates the full directory tree and is idempotent (safe to run multiple times):
from pathlib import Path

Path("logs/2026/06").mkdir(parents=True, exist_ok=True)
This call:
  • Creates logs if missing, then logs/2026, then logs/2026/06.
  • Returns successfully if any of those already exist as directories.
  • Raises FileExistsError only if a non-directory (typically a regular file) sits at any target path.
The behavior matches Unix mkdir -p exactly. It's the right call for nearly every "create this directory if needed" scenario in 2026.

What Each Flag Does

parents=True

Without it, Path('a/b/c').mkdir() requires both a and a/b to exist. If they don't, the call raises FileNotFoundError. The flag tells pathlib to create the whole chain in one operation.
Path("project/data/raw").mkdir(parents=True)
# Creates project, project/data, and project/data/raw if any are missing
The default is False because creating directories the user didn't explicitly name could mask typos. Path('logz/today').mkdir() should fail loudly if logz is a typo for logs, not silently produce a wrong tree. Once you confirm the path is correct, opt into parents=True.

exist_ok=True

Without it, mkdir on a path that's already a directory raises FileExistsError. The flag makes the call idempotent.
Path("logs").mkdir(exist_ok=True)
Path("logs").mkdir(exist_ok=True)    # Second call also succeeds
This is the flag you nearly always want for "ensure this directory exists" semantics. The naming is precise: existing is OK, treat it as success.
The file-in-the-way trap: exist_ok=True only suppresses the error when a DIRECTORY already exists at the target path. If a regular file sits there, FileExistsError still fires. This is correct behavior; mkdir can't replace a file with a directory, and silencing this case would mask real bugs.

Reading the Truth Table

The matrix above maps the four parameter combinations against four scenarios. Two takeaways:
  1. The canonical pair (parents=True, exist_ok=True) handles three of the four scenarios successfully.
  2. The fourth scenario (file in the way) always errors regardless of flags. This is by design.
For most code, the right behavior is to let FileExistsError propagate when a file blocks the path. It usually indicates a configuration bug or a misnamed path. Don't catch and ignore this exception broadly; if you must handle it, log loudly and stop.

The os.makedirs Alternative

Before pathlib became standard, the procedural equivalent was os.makedirs. The function still works and is identical in behavior:
import os

os.makedirs("logs/2026/06", exist_ok=True)
# Equivalent to:
# Path("logs/2026/06").mkdir(parents=True, exist_ok=True)
Two differences worth noting:
  • os.makedirs always creates intermediate parents; there's no separate flag. Path.mkdir defaults to no-parents and requires parents=True to enable.
  • Both accept exist_ok=True; the semantics match.
For new code in 2026, prefer Path.mkdir because it integrates with the rest of pathlib. For legacy modules that use os.path and os.makedirs consistently, stay with what's there until you can migrate the whole file.

The mode Parameter

Both Path.mkdir and os.makedirs accept a mode parameter for the directory's permission bits. The default is 0o777 masked by the process's umask, which on most systems produces 0o755 (rwxr-xr-x).
Path("private").mkdir(mode=0o700, parents=True, exist_ok=True)
# Owner read/write/execute, no group or world access
Use this for directories holding sensitive data: caches with credentials, per-user scratch space, anything you don't want other users on the system to read. Note that on Windows the mode is largely ignored; use proper ACLs if you need fine-grained control there.

One caveat: parents=True creates intermediate directories with the DEFAULT mode (umask-adjusted 0o777), NOT the mode you pass. If you need restrictive permissions on the whole chain, create each intermediate explicitly.

Race Conditions and Concurrency

The mkdir syscall itself is atomic: two processes racing to create the same directory will produce one success and one error. With exist_ok=True, the loser's error is suppressed, so both calls return successfully. For most concurrent use cases, this is the behavior you want.

One known edge case: when parents=True is used and an intermediate directory is deleted by another process between the implementation's internal mkdir call and the subsequent is_dir() check, FileExistsError can fire spuriously. CPython bug #29694 and subsequent reports tracked this; patches landed in 3.13 maintenance releases. For production code expecting heavy concurrency on shared paths, wrap the call in retry logic:
import time

def mkdir_with_retry(path: Path, attempts: int = 3) -> None:
    for attempt in range(attempts):
        try:
            path.mkdir(parents=True, exist_ok=True)
            return
        except FileExistsError:
            if path.is_dir():
                return    # success after race
            if attempt == attempts - 1:
                raise
            time.sleep(0.01)
For most application code, the plain one-liner is fine. Reach for the retry pattern only when benchmarks or stack traces show the race actually happens.

Real-World Patterns

Standard application directory layout at startup

from pathlib import Path

APP_ROOT = Path.home() / ".myapp"
LOGS = APP_ROOT / "logs"
CACHE = APP_ROOT / "cache"
DATA = APP_ROOT / "data"

for dir in (APP_ROOT, LOGS, CACHE, DATA):
    dir.mkdir(parents=True, exist_ok=True)
Run at startup. Idempotent: starts fine on the first run (creates the tree) and on the hundredth run (does nothing).

Date-partitioned log directories

from datetime import date

def log_dir_for_today() -> Path:
    today = date.today()
    path = Path("logs") / f"{today.year:04d}" / f"{today.month:02d}" / f"{today.day:02d}"
    path.mkdir(parents=True, exist_ok=True)
    return path
Called once per log write; the per-day creation is idempotent and cheap.

Per-user scratch directory with restrictive permissions

import os
from pathlib import Path

scratch = Path(f"/tmp/scratch-{os.getuid()}")
scratch.mkdir(mode=0o700, exist_ok=True)
The mode ensures other users on the system can't read the scratch contents.

Secure Temporary Directories: tempfile.mkdtemp

For directories that need to be unique, secure, and short-lived, mkdir isn't the right tool. Use tempfile.mkdtemp instead. It creates a directory with a guaranteed-unique name and restrictive permissions (0o700) in the system temp area:
import tempfile
from pathlib import Path

scratch = Path(tempfile.mkdtemp(prefix="myapp-"))
# /tmp/myapp-a1b2c3d4 on Linux, with mode 0700
print(scratch.exists(), scratch.stat().st_mode & 0o777)
# True, 448 (which is 0o700)
For the context-manager form that cleans up on exit, use tempfile.TemporaryDirectory:
with tempfile.TemporaryDirectory(prefix="myapp-") as tmp:
    workspace = Path(tmp)
    # ... use workspace ...
# Directory is automatically removed on exit, even on exceptions
For workflows that need atomic file replacement (write to a temp file in the same directory, then rename to the target), tempfile with the dir= argument keeps the temp on the same filesystem as the target, ensuring the rename is atomic.

Cross-Platform Caveats

pathlib normalizes path separators automatically, so Path('a/b/c') and Path('a', 'b', 'c') produce the right separators on every platform. But a few cross-platform issues are worth knowing.

Windows path length

The traditional Windows path-length limit is 260 characters (MAX_PATH). On modern Windows 10+ with long-path support enabled, the limit lifts to ~32,000 characters. mkdir on a deeply nested path can still fail on systems where long paths are not enabled. The fix: enable long paths in the registry, or use the \\?\ prefix on Windows-specific code paths.

Mode parameter on Windows

The mode argument is largely ignored on Windows because the platform uses ACLs (Access Control Lists), not POSIX permissions. If you need fine-grained access control on Windows, use win32security from the pywin32 package after creating the directory.

Case-sensitivity differences

macOS HFS+ (default until 2017) and Windows NTFS are case-INSENSITIVE by default; Linux ext4 and macOS APFS are case-SENSITIVE. Path('Logs').mkdir() followed by Path('logs').mkdir(exist_ok=True) succeeds on Windows and case-insensitive macOS volumes (treats them as the same directory) but creates two separate directories on Linux. Be consistent with case to avoid surprises.

Common Mistakes

1. Forgetting parents=True

Path("logs/2026/06").mkdir(exist_ok=True)
# FileNotFoundError if "logs/2026" doesn't already exist
If you write multi-segment paths, parents=True is almost always what you want.

2. Catching FileExistsError when exist_ok=True would suffice

# Bad: re-implementing exist_ok with broader error swallowing
try:
    Path("logs").mkdir()
except FileExistsError:
    pass

# Good: use the flag
Path("logs").mkdir(exist_ok=True)
The flag is precise; the try/except can swallow more than you intend (e.g., file-in-the-way bugs).

3. Assuming exist_ok=True silences file-in-the-way

# If "logs" is a regular file, this STILL raises FileExistsError:
Path("logs").mkdir(exist_ok=True)
This is by design. If you encounter this case, log the conflict and decide whether to rename, delete, or fail.

4. Using mkdir for ensure_dir patterns when you should rename

For atomic-write workflows (write to temp, rename to target), the directory needs to exist BEFORE the temp file is created. Use mkdir(parents=True, exist_ok=True) early in setup, not interleaved with writes.

5. Creating directories with overly permissive mode on shared systems

The default mode=0o777 masked by umask is fine for most cases. For directories holding sensitive data on multi-user systems, pass mode=0o700 explicitly. Don't rely on umask alone if security matters.

Frequently Asked Questions

What is the Python equivalent of mkdir -p?

Path('path/to/dir').mkdir(parents=True, exist_ok=True) is the canonical one-liner. parents=True tells pathlib to create any missing intermediate directories (the -p part of mkdir -p). exist_ok=True tells it to treat "directory already exists" as success instead of raising FileExistsError. Together the call creates the full directory tree and is idempotent: running it again produces the same result. The os.makedirs(path, exist_ok=True) function gives the same behavior with the procedural API; pathlib is preferred for new code in 2026.

What does parents=True do in Python's Path.mkdir()?

parents=True tells pathlib to create any missing parent directories on the way to the target. Without it, Path('a/b/c').mkdir() raises FileNotFoundError if a or a/b don't exist. With it, the full chain is created in one call. This matches the behavior of Unix mkdir -p. The flag is False by default because creating directories the user didn't explicitly name could mask typos: Path('logz/today').mkdir() should fail loudly if 'logz' is a typo for 'logs', not silently create the wrong tree.

What does exist_ok=True do in Python's Path.mkdir()?

exist_ok=True tells pathlib to silently succeed when the target directory already exists. Without it, calling mkdir() on a path that's already a directory raises FileExistsError. With it, the call is idempotent: a second mkdir() on the same path produces no error. Important caveat: exist_ok=True only suppresses the error when the existing path is a DIRECTORY. If a FILE exists at that path, FileExistsError still fires because mkdir can't replace a file with a directory. This is the file-in-the-way trap.

What is the difference between Path.mkdir() and os.makedirs() in Python?

Functionally they do the same job: create a directory tree. The differences: Path.mkdir() is a method on a Path object, integrates with the rest of pathlib (parent, with_suffix, joinpath chain naturally), and is the modern PEP 428 form. os.makedirs() is a procedural function that takes a string path and predates pathlib. Both accept exist_ok=True; pathlib also splits parent creation into a separate parents=True flag while os.makedirs creates parents unconditionally. For new code in 2026, use Path.mkdir(); for legacy modules consistent with os.makedirs everywhere, stay consistent.

Does mkdir(exist_ok=True) work with concurrent processes in Python?

Mostly. The C-level mkdir syscall is atomic, so racing two mkdir(exist_ok=True) calls from different processes resolves cleanly: one succeeds and the other sees the directory now exists and treats that as success. However, a known CPython issue (tracked in bug #29694 and subsequent reports) showed that when parents=True is used and an intermediate directory is deleted between the implementation's internal mkdir and the follow-up is_dir() check, FileExistsError can fire incorrectly. The fix landed in CPython 3.13 patch releases. For production code expecting heavy concurrency, wrap the call in retry logic or use temporary directory + rename patterns for safer atomicity.

The Bottom Line: One Line for 95% of Cases

Path('p').mkdir(parents=True, exist_ok=True) handles nearly every directory-creation case in modern Python. parents=True creates missing intermediates; exist_ok=True makes the call idempotent. The fourth scenario (a regular file sitting at the target path) always errors regardless of flags; this is correct, not a bug. Use os.makedirs(path, exist_ok=True) for legacy code. Reach for retry patterns only when concurrent races are real. Next: finding the directory your Python script lives in. Or browse the full Stdlib Essentials guide.

Build Directory-Setup Reflexes

CodeGym's Python track turns pathlib directory patterns into reflex across 62 levels: idempotent setup, atomic writes, mode-controlled scratch dirs. AI validators catch the LBYL anti-patterns and the file-in-the-way traps; AI mentors explain why one form is preferred over another. First level free; full plan on the pricing page.