How to Create Directories in Python: Absolute Beginners Tutorial

Final Exam & Certification

Complete this tutorial and pass the 10-question final exam to earn a downloadable certificate of completion.

skip to exam

Python gives you two standard ways to create directories: the os module, which has been available since early Python, and pathlib, introduced in Python 3.4 as a more modern, object-oriented alternative. This tutorial covers both from the ground up.

When a Python script needs to save output files, organize data, or prepare a project layout, it often needs to create directories at runtime. Understanding which tool to reach for — and what errors to expect — is one of those foundational skills that shows up constantly in real code.

Before you start — what you need

This tutorial is designed for absolute beginners. You need: Python 3.5 or newer installed on your computer (check by running python3 --version or python --version in your terminal), and a text editor or IDE to write your scripts (VS Code, IDLE, PyCharm, or any plain text editor will work). To run the examples, save each snippet as a .py file and run it with python3 yourfile.py from your terminal. No packages to install — everything used here ships with Python.

What is a Directory in Python?

A directory is a folder on your file system. Python uses the term interchangeably with "folder" in its documentation. When you create a directory in Python, you are instructing the operating system to reserve a named location in the file system where files and other directories can be stored.

Python does not have a built-in keyword for directory operations. Instead, the standard library provides two modules that handle file system work: os and pathlib. You need to import at least one of them before creating any directories.

Standard Library

Both os and pathlib ship with Python — no third-party installation is required. You get them with any standard Python 3 install.

decision guide answer the questions to find the right function

Do you need to create more than one directory level (for example, data/reports/2026 where none of those folders exist yet)?

Creating a Single Directory with os.mkdir()

os.mkdir() is the most direct way to create a single directory. It accepts a path string and creates exactly one new directory at that location. If any part of the parent path does not already exist, Python raises FileNotFoundError. If the directory already exists, Python raises FileExistsError.

python
import os

# Create a single directory named "output" in the current working directory
os.mkdir("output")
print("Directory created.")
Try it now — expected output

Save the code above as make_dir.py and run it with python3 make_dir.py. You should see:

Directory created.

After it runs, check your file manager or run ls (Mac/Linux) or dir (Windows) in the same folder — you will see a new folder called output. Run the script a second time and you will see the FileExistsError described below, because the folder already exists.

If output already exists when this code runs, Python will raise FileExistsError: [Errno 17] File exists: 'output'. The solution is to check first or use exist_ok, which is covered in the next section.

Handling errors with os.mkdir()

Wrapping the call in a try-except block lets your script continue gracefully when the directory is already present or when permissions prevent creation.

python
import os

try:
    os.mkdir("output")
    print("Directory created successfully.")
except FileExistsError:
    print("Directory already exists — skipping creation.")
except PermissionError:
    print("Permission denied. Cannot create directory here.")
except OSError as e:
    print(f"OS error: {e}")

What you will see the first time you run this (when output does not yet exist):

Directory created successfully.

What you will see every time after that (when output already exists):

Directory already exists — skipping creation.
Pro Tip

FileExistsError is a subclass of OSError. Catching OSError alone will also catch FileExistsError and PermissionError, but naming them separately makes your intent clearer to anyone reading the code.

spot the bug click the line that contains the error

The code below tries to create a directory and print a message on success, but it has one error. Which line is wrong?

1 import os
2
3 os.mkdir("logs", "backup")
4 print("Done.")
Why: os.mkdir() accepts only one path argument. Passing two strings raises TypeError: mkdir() takes from 1 to 2 positional arguments but 3 were given (the extra argument is mode, not a second path). To create two directories, call os.mkdir() twice or use os.makedirs() for nested paths.

Creating Nested Directories with os.makedirs()

os.makedirs() is the recursive version of os.mkdir(). When you pass a path like "data/reports/2026", Python creates all three directories in a single call — even if none of them exist yet. This is equivalent to the shell command mkdir -p.

python
import os

# Creates data/, data/reports/, and data/reports/2026 — all in one call
os.makedirs("data/reports/2026")
print("All directories created.")

Expected output:

All directories created.

After running this, your working directory will contain a new data/ folder, inside which is reports/, inside which is 2026/. Run it a second time without exist_ok=True and you will see FileExistsError: [Errno 17] File exists: 'data/reports/2026'. That is what the next section solves.

The exist_ok parameter

By default, os.makedirs() raises FileExistsError if the target directory already exists, exactly like os.mkdir(). Passing exist_ok=True tells Python to silently skip creation when the directory is already present — no exception, no fuss.

python
import os

# Safe to call even if the directory already exists
os.makedirs("data/reports/2026", exist_ok=True)
print("Directory is ready.")

Expected output — every time you run it, whether the directory exists or not:

Directory is ready.
Watch Out

exist_ok=True only suppresses the error when the path already exists as a directory. If the path exists as a file, Python still raises OSError. The same rule applies to pathlib.Path.mkdir().

exist_ok=True does not protect you against broken symlinks

There is a subtle edge case that catches experienced developers: if the target path is a broken symlink — a symlink whose destination has been deleted — os.makedirs(path, exist_ok=True) raises FileExistsError even though os.path.exists(path) returns False for that same path. The symlink entry occupies the path in the directory, so the kernel reports EEXIST when Python attempts mkdir, but the resolution check inside Python sees no target and considers the path absent. This inconsistency is a known CPython issue (GitHub issue #129626, filed February 2025 and closed as not planned, meaning it remains an unfixed quirk of the implementation). If your code runs in an environment where symlinks may be left dangling — containers, mounted volumes, test fixtures — check explicitly with os.path.islink(path) before calling makedirs.

file system visualizer watch Python build directories step by step

os.mkdir("data/reports/2026") — parent directories do not exist yet. Watch what happens.

code builder click a token to place it

Build the correct call to create nested directories safely, suppressing the error if they already exist:

your code will appear here...
exist_ok=True) os.mkdir( os.makedirs( "logs/archive", parents=True, exist_ok=False)
Why: The correct call is os.makedirs("logs/archive", exist_ok=True). os.makedirs() handles nested paths recursively. exist_ok=True prevents FileExistsError when the directory already exists. os.mkdir() would only work if logs/ already exists and there is no nesting involved. parents=True is the pathlib equivalent — it does not belong in an os.makedirs() call.

The Modern Approach: pathlib.Path.mkdir()

The pathlib module, introduced in Python 3.4, provides a Path class that represents file system paths as objects rather than plain strings. This makes path construction, inspection, and manipulation more readable and cross-platform by default.

The method for creating a directory with pathlib is Path.mkdir(). It accepts two keyword arguments that correspond directly to the behavior of os.makedirs(): parents and exist_ok.

python
from pathlib import Path

# Create a single directory
p = Path("output")
p.mkdir()

# Create nested directories, no error if they already exist
nested = Path("data/reports/2026")
nested.mkdir(parents=True, exist_ok=True)
print("Directories ready.")

Expected output:

Directories ready.

Notice that parents=True in pathlib corresponds to the recursive behavior of os.makedirs(). Without parents=True, Path.mkdir() behaves like os.mkdir() and raises FileNotFoundError if any parent directory is missing.

predict the output think before you reveal

Neither a/ nor a/b/ exist on disk. You run: Path("a/b/c").mkdir() — without parents=True. What error is raised? Then you run Path("a/b/c").mkdir(parents=True). The directory a/b/c now exists. You run it again. What happens?

Path("a/b/c").mkdir() # FileNotFoundError: [Errno 2] No such file or directory: 'a/b/c' # (parent a/b/ does not exist — parents=False is the default) Path("a/b/c").mkdir(parents=True) # Succeeds — creates a/, a/b/, and a/b/c/ in one call Path("a/b/c").mkdir(parents=True) # FileExistsError: [Errno 17] File exists: 'a/b/c' # (parents=True does not imply exist_ok=True — they are independent)

The key insight is that parents=True and exist_ok=True are completely independent parameters. parents=True only controls whether missing intermediate directories are created. It says nothing about what to do when the target already exists. You need exist_ok=True separately to suppress FileExistsError. In production code, Path.mkdir(parents=True, exist_ok=True) is almost always what you want — it covers both cases at once.

The Path class has a built-in method for checking whether a path exists and is a directory. This can be useful before performing operations that require the directory to be present.

python
from pathlib import Path

p = Path("output")

# Create only if absent
if not p.is_dir():
    p.mkdir()
    print("Created output/")
else:
    print("output/ already exists.")
"The aim of this library is to provide a simple hierarchy of classes to handle filesystem paths and the common operations users do over them." — Antoine Pitrou, PEP 428 — The pathlib module (Python Software Foundation, 2012)
code builder click a token to place it

Build the correct pathlib call to create logs/archive/2026 — creating all missing parents — and suppress any error if the directory already exists:

your code will appear here...
Path("logs/archive/2026").mkdir( parents=True, exist_ok=True) recursive=True, exist_ok=False) makedirs("logs/archive/2026",
Why: The correct call is Path("logs/archive/2026").mkdir(parents=True, exist_ok=True). In pathlib, parents=True enables recursive creation of all missing intermediate directories — it is the pathlib equivalent of os.makedirs(). exist_ok=True suppresses FileExistsError if the leaf directory already exists. recursive=True does not exist in pathlib. makedirs() is an os module function and is not a method on Path objects.

One important version detail: exist_ok was added to os.makedirs() in Python 3.2, but it was not added to Path.mkdir() until Python 3.5. If you are writing code that must target Python 3.4 specifically, use os.makedirs() with exist_ok=True rather than Path.mkdir(exist_ok=True). In any Python 3.5 or later environment — which is everything in current practice — both work identically.

Under the Hood

According to CPython's source, Path.mkdir() creates directories by calling os.mkdir() internally. When parents=True is set, it calls itself recursively to create each parent with os.mkdir(). This means both approaches ultimately reach the same operating system call — the difference is entirely in the Python layer above it.

call stack trace click each layer to expand — from your Python down to the kernel

When you write Path("output").mkdir(), Python executes a chain of calls that ends at the operating system kernel. Each layer below is collapsible.

Path("output").mkdir()

You call Path.mkdir() on a Path object. The Path class is defined in Lib/pathlib.py in CPython's standard library. No OS call has been made yet — this is pure Python object method dispatch.

Both os.mkdir() and Path.mkdir() accept an optional mode parameter that specifies the permission bits for the new directory. The default is 0o777, which is then modified by the process's current umask.

How umask actually modifies permissions — it is not subtraction

The common shorthand "777 minus umask" works for simple cases but is technically wrong. The kernel computes actual permissions as mode & ~umask — a bitwise AND with the complement of the umask. The arithmetic happens to match subtraction when no bit "borrows" into a neighboring group, which is why the shorthand survives in documentation. There is a second factor most tutorials skip entirely: if the parent directory has a default ACL (Access Control List) set, the umask is completely ignored for new entries in that directory. Instead, the default ACL is inherited and used as the permission basis. This is relevant on Linux systems where administrators use setfacl -d on shared directories to enforce consistent permissions regardless of each process's umask. You can check whether a directory has a default ACL with getfacl <directory> from the shell.

python
import os
from pathlib import Path

# Create a directory readable/writable/executable only by the owner (Unix)
os.mkdir("private_data", mode=0o700)

# Same with pathlib
Path("secure_logs").mkdir(mode=0o700, exist_ok=True)
Mode and os.makedirs()

When using os.makedirs(), the mode parameter is only applied to the leaf (final) directory, not to the intermediate parent directories created along the way. If you need to set permissions on newly created parent directories, set the umask before calling os.makedirs() and restore it afterward. The permission bits of any already-existing parent directories are never changed by either function.

Python 3.7 silently changed how mode works in os.makedirs()

Before Python 3.7, the mode argument was passed to every newly created directory in the path — intermediate parents included. Starting in Python 3.7, the mode argument is passed only to the leaf directory; intermediate directories are created with the process default (umask applied to 0o777). This is documented in the official Python changelog but caught production codebases off guard when 3.7 shipped — Django's FileSystemStorage had a related bug filed against it as a result. If you are passing a non-default mode to os.makedirs() and need that mode applied to every directory in the chain, call os.chmod() on each directory afterward, or set the umask before the call.

permission calculator see exactly what permissions your directory gets

Enter a mode value and a umask. The calculator applies mode & ~umask and shows the final permission bits with a full explanation.

& ~
=

The Python documentation explicitly warns that os.makedirs() will become confused if any path element in the chain is .. (the parent-directory reference, equivalent to os.pardir). The confusion arises because os.makedirs() strips path components one at a time from the right to find what it needs to create, and a .. component causes that traversal to produce incorrect results. For example, os.makedirs("a/b/../c") may attempt to create paths in the wrong order or raise errors unexpectedly. This is an officially documented limitation that has been present since the function was introduced. The fix is to normalize the path before passing it: use Path("a/b/../c").resolve() or os.path.abspath("a/b/../c"), both of which expand the path to its canonical form first.

os module vs. pathlib — side-by-side comparison

The table below compares the two approaches across key scenarios. Click any row to see a short explanation.

os: os.mkdir("folder")
pathlib: Path("folder").mkdir()

Both create one directory. Both raise FileExistsError by default if the folder already exists. pathlib requires the parent path to exist, just like os.mkdir().

os: os.makedirs("a/b/c")
pathlib: Path("a/b/c").mkdir(parents=True)

Both create all intermediate directories in a single call. The os approach uses a separate function name; pathlib uses a keyword argument on the same method.

os: os.makedirs("folder", exist_ok=True)
pathlib: Path("folder").mkdir(exist_ok=True)

Both accept exist_ok=True. When set, no exception is raised if the directory is already present. This is the most common pattern in production scripts.

os: os.path.isdir("folder")
pathlib: Path("folder").is_dir()

Both return True if the path exists and is a directory. The pathlib version is a method on the object, which fits naturally when you are already working with a Path instance.

os: os.path.join("parent", "child")
pathlib: Path("parent") / "child"

pathlib uses the / operator for path joining, which many developers find more readable. The os approach uses a function call. Both handle OS-specific separators correctly.

Absolute Paths vs Relative Paths

Every example so far has used a name like "output" or "data/reports/2026" — these are relative paths. A relative path is resolved from the process's current working directory, which is wherever your terminal was when you ran the script, not where the script file lives on disk. An absolute path starts from the filesystem root (/ on Unix, a drive letter on Windows) and means the same thing no matter where you run it from.

python
import os
from pathlib import Path

# Find out where Python thinks you currently are
print(os.getcwd())       # e.g. /home/user/projects
print(Path.cwd())        # same thing, as a Path object

# Relative path — resolved from cwd at runtime
os.makedirs("output/logs", exist_ok=True)
# If cwd is /home/user/projects this creates /home/user/projects/output/logs

# Absolute path — always the same regardless of cwd
os.makedirs("/tmp/myapp/logs", exist_ok=True)
Relative paths and where they land

A very common beginner mistake is running a script from a different directory than expected and finding the new folder was created in the wrong place. If your script must create a directory in a predictable location, either use an absolute path or anchor to Path(__file__).parent.resolve() as shown in the real-world patterns section.

path resolver type a path to see exactly where Python would create it

Enter a working directory and a path argument. The tool resolves where Python would actually create the directory.

Where your terminal is when the script runs
The string you pass to os.makedirs() or Path()

Python handles spaces in directory names without any special syntax. You do not need to escape spaces or use quotes beyond the normal Python string rules. The only place spaces cause friction is when you reference the directory from a shell command — there you would need to quote the path. Inside Python code itself, spaces in directory names are just ordinary characters in a string.

python
import os
from pathlib import Path

# Spaces in names work fine in Python — no escaping needed
os.makedirs("my project/output files", exist_ok=True)

# pathlib handles it just as easily
Path("my project/output files").mkdir(parents=True, exist_ok=True)

# The / operator in pathlib also handles spaces correctly
base = Path("my project")
(base / "output files").mkdir(parents=True, exist_ok=True)

os.path.exists() vs os.path.isdir() — an important distinction

Many beginners reach for os.path.exists() to check whether a path is safe to use as a directory. This is a subtle bug waiting to happen. os.path.exists() returns True for anything at that path — a file, a directory, a symlink, a named pipe. os.path.isdir() returns True only when the path exists and is specifically a directory. If a file named output already exists and your code checks os.path.exists("output") before deciding whether to create the directory, it will skip creation, then fail later when it tries to write files into a path that is actually a file.

python
import os
from pathlib import Path

# Imagine "output" already exists as a FILE (not a directory)
os.path.exists("output")   # True  — but it's a file!
os.path.isdir("output")    # False — correctly identifies it is not a directory

Path("output").exists()    # True  — same issue
Path("output").is_dir()    # False — correct

# The safest approach: skip the check entirely and use exist_ok=True.
# exist_ok=True still raises OSError if a *file* is in the way,
# which is exactly the behavior you want.
os.makedirs("output", exist_ok=True)  # Raises OSError if "output" is a file
The right rule

When working with directories specifically, always use os.path.isdir() or Path.is_dir() rather than the generic exists() check. Even better: skip the check entirely and use exist_ok=True, which handles the file-vs-directory conflict correctly without a separate check step.

race condition visualizer TOCTOU — time-of-check to time-of-use

The unsafe pattern if not os.path.isdir(path): os.mkdir(path) has a gap between the check and the creation. In concurrent environments, another process can act in that gap. Watch it happen below.

Creating a directory is only the first step. A complete understanding of directory operations in Python means knowing how to list its contents, write files into it, rename or move it, and remove it when it is no longer needed.

Listing directory contents

os.listdir() returns a plain list of name strings with no guaranteed order. Path.iterdir() returns an iterator of Path objects — more useful when you need to do anything with the items beyond printing their names, because you get type-checking methods like .is_dir() and .is_file() directly on each result.

python
import os
from pathlib import Path

# os.listdir() — list of name strings, no guaranteed order
names = os.listdir("output")
print(names)  # ['report.txt', 'data.csv', 'archive']

# Path.iterdir() — iterator of Path objects, sorted here for readability
p = Path("output")
for item in sorted(p.iterdir()):
    kind = "DIR " if item.is_dir() else "FILE"
    print(f"  {kind}  {item.name}")

Writing a file into a newly created directory

predict the output think before you reveal

A directory called output/ exists on disk and contains two items: a subdirectory called archive/ and a file called report.txt. What does sorted(Path("output").iterdir()) return? What types are the items?

[PosixPath('output/archive'), PosixPath('output/report.txt')]

Each item is a Path object (specifically PosixPath on Unix, WindowsPath on Windows) — not a plain string. Sorting is alphabetical by path, so archive comes before report.txt. You can call .is_dir() and .is_file() directly on each result because they are full Path objects, not just names. This is why Path.iterdir() is more useful than os.listdir() for anything beyond printing names.

The most common next step after creating a directory is writing a file into it. With pathlib, this is a single readable chain: create the directory, build the file path with the / operator, then call .write_text() or use open() normally.

python
from pathlib import Path

output_dir = Path("output/reports")
output_dir.mkdir(parents=True, exist_ok=True)

# Write a text file directly into the new directory
report = output_dir / "summary.txt"
report.write_text("Report generated.\n", encoding="utf-8")

# Or use open() the standard way
with open(output_dir / "log.txt", "w", encoding="utf-8") as f:
    f.write("Log entry.\n")

print(f"Files written to {output_dir.resolve()}")

Renaming and moving a directory

To rename a directory in the same parent, use Path.rename() or os.rename(). To move it to a different location — including across drives — use shutil.move(). The key difference: os.rename() is atomic but only works within the same filesystem. shutil.move() handles cross-device moves by falling back to copy-then-delete, making it the safe universal choice.

python
import shutil
from pathlib import Path

# Rename within the same parent — returns the new Path object (Python 3.8+)
old = Path("output")
new = old.rename("output_archive")

# Move to a different location, including across filesystems
shutil.move(new, Path("/tmp/backup/output_archive"))

Deleting a directory

Python provides three deletion tools. os.rmdir() and Path.rmdir() remove a single empty directory — both raise OSError if any files or subdirectories remain inside. os.removedirs() walks upward through the path removing empty parents until it hits one that is not empty. shutil.rmtree() deletes a directory and everything inside it recursively. It has no undo — nothing goes to a trash folder.

python
import os
import shutil
from pathlib import Path

# Remove a single EMPTY directory
os.rmdir("empty_folder")       # raises OSError if not empty
Path("empty_folder").rmdir()   # same rule — must be empty

# Remove a chain of empty parent directories upward
os.removedirs("parent/child/grandchild")

# Remove a directory AND everything inside it — PERMANENT, no undo
shutil.rmtree("output_v1")

# Safer pattern: check before deleting recursively
target = Path("output_v1")
if target.is_dir():
    shutil.rmtree(target)
    print(f"Deleted {target}")
shutil.rmtree() is permanent

shutil.rmtree() does not move anything to a trash or recycle bin — the files are gone immediately. Always validate the path before calling it, especially when it is constructed from user input or environment variables. A typo in the path can delete unintended directories with no warning.

predict the output think before you reveal

The directory data/ exists and contains one file. You run: os.rmdir("data"). What happens? Then you run os.removedirs("data"). What happens differently?

os.rmdir("data") # OSError: [Errno 39] Directory not empty: 'data' os.removedirs("data") # OSError: [Errno 39] Directory not empty: 'data'

Both raise OSError because the directory is not empty — os.rmdir() and os.removedirs() only delete directories that contain nothing at all. The difference between them matters in a different scenario: os.removedirs("a/b/c") tries to remove c, then b, then a — stopping at the first non-empty ancestor. os.rmdir() only attempts one level. Neither removes directory contents; that is exclusively shutil.rmtree().

On Windows, paths use backslash (\) as the separator; on macOS and Linux they use forward slash (/). Hardcoding either separator in string literals is a fast way to create code that only works on one platform. The pathlib / operator always produces the correct separator for the current OS automatically.

python
from pathlib import Path
import os

# Works identically on Windows, macOS, and Linux
target = Path("data") / "reports" / "2026"
target.mkdir(parents=True, exist_ok=True)

# os.path.join() is the os-module equivalent — also cross-platform
os.makedirs(os.path.join("data", "reports", "2026"), exist_ok=True)

# Avoid hardcoded separators — these break on the wrong platform:
# os.makedirs("data\\reports\\2026")  # fails on Unix
# os.makedirs("data/reports/2026")    # works on both but pathlib is cleaner
error simulator select a scenario — see the exact error Python raises and why

Pick a situation. The simulator shows the code, the exact traceback Python would produce, and the explanation of what went wrong.

code
output

How to Create Directories in Python

Follow these five steps to reliably create directories in Python, whether you are using the os module or pathlib.

  1. Import the required module

    At the top of your script, write import os to use the os module, or from pathlib import Path to use pathlib. Both modules are part of Python's standard library — no installation required.

  2. Define the directory path

    For the os module, write the path as a string: "data/output". For pathlib, create a Path object: Path("data/output"). Use forward slashes — Python and pathlib normalize them across Windows, macOS, and Linux automatically.

  3. Call the directory creation function

    For a single directory whose parent already exists, use os.mkdir("folder") or Path("folder").mkdir(). For nested directories, use os.makedirs("parent/child", exist_ok=True) or Path("parent/child").mkdir(parents=True, exist_ok=True).

  4. Add error handling

    If you are not using exist_ok=True, wrap the call in a try-except block that catches at minimum FileExistsError and PermissionError. In automated scripts and pipelines, exist_ok=True is almost always the right choice because it makes the operation idempotent — safe to run more than once.

  5. Verify the directory was created

    Confirm the result with os.path.isdir("folder") or Path("folder").is_dir(). Both return True if the directory is present. This step is optional in most scripts but useful when debugging or writing tests.

Here is a complete, production-ready pattern that combines all five steps using pathlib:

python
from pathlib import Path

def ensure_directory(path_str: str) -> Path:
    """Create a directory (and any parents) if it does not exist.
    Returns the Path object on success.
    """
    p = Path(path_str)
    try:
        p.mkdir(parents=True, exist_ok=True)
    except PermissionError as e:
        raise RuntimeError(f"Permission denied: cannot create {p}") from e
    return p

# Usage
output_dir = ensure_directory("data/reports/2026")
print(f"Ready: {output_dir}")
print(f"Is a directory: {output_dir.is_dir()}")

Guided Exercise: Build a Project Folder Structure from Scratch

This exercise walks you through every step of building a real directory structure using Python — from opening a blank file to verifying the result. Follow each step in order on your own machine. By the end, you will have created a six-folder project layout, written a file into it, verified it exists, and cleaned up after yourself.

What you will build

A my_project/ folder containing: data/raw/, data/processed/, output/reports/, and logs/. You will then write a readme file into the project root, list the contents, and delete everything when done.

Step 1 — Create a new Python file

Open your text editor or terminal. Create a new file called build_project.py in any folder you can easily find — your Desktop or a practice/ folder works well. Leave it empty for now.

Step 2 — Check where you are before you start

Add the following to build_project.py and run it first:

python
from pathlib import Path

# Step 2: Confirm where your script will create folders
print("Working directory:", Path.cwd())

Run it: python3 build_project.py

Expected output (the exact path will be your own):

Working directory: /Users/yourname/practice

This confirms Python will create your folders relative to that location. If you see a path you did not expect, cd to the folder containing build_project.py before continuing.

Step 3 — Create the full directory tree in one call

Replace the contents of build_project.py with the code below, then run it:

python
from pathlib import Path

# Step 3: Create the full project directory tree
base = Path("my_project")

folders = [
    base / "data" / "raw",
    base / "data" / "processed",
    base / "output" / "reports",
    base / "logs",
]

for folder in folders:
    folder.mkdir(parents=True, exist_ok=True)
    print(f"  Created: {folder}")

print("Done.")

Expected output:

  Created: my_project/data/raw
  Created: my_project/data/processed
  Created: my_project/output/reports
  Created: my_project/logs
Done.

Open your file manager (or run find my_project in the terminal) and you will see the full nested tree. Run the script a second time — you will see the same output and no errors, because exist_ok=True makes the operation safe to run repeatedly.

Step 4 — Verify the structure in code

Add a verification step that confirms each folder exists before you try to use it:

python
from pathlib import Path

base = Path("my_project")

# Step 4: Verify each folder exists and is a directory
for folder in sorted(base.rglob("*")):
    if folder.is_dir():
        print(f"  OK  {folder}")

Expected output:

  OK  my_project/data
  OK  my_project/data/processed
  OK  my_project/data/raw
  OK  my_project/logs
  OK  my_project/output
  OK  my_project/output/reports

Step 5 — Write a file into the project

python
from pathlib import Path

base = Path("my_project")

# Step 5: Write a README into the project root
readme = base / "README.txt"
readme.write_text("My project folder. Created with Python.\n", encoding="utf-8")
print(f"Wrote: {readme}")
print(f"Contents: {readme.read_text(encoding='utf-8')}")

Expected output:

Wrote: my_project/README.txt
Contents: My project folder. Created with Python.

Step 6 — List all contents including files

python
from pathlib import Path

base = Path("my_project")

# Step 6: List everything — files and folders
for item in sorted(base.rglob("*")):
    kind = "[DIR] " if item.is_dir() else "[FILE]"
    print(f"  {kind} {item}")

Expected output:

  [FILE] my_project/README.txt
  [DIR]  my_project/data
  [DIR]  my_project/data/processed
  [DIR]  my_project/data/raw
  [DIR]  my_project/logs
  [DIR]  my_project/output
  [DIR]  my_project/output/reports

Step 7 — Clean up (optional)

When you are finished practicing, run this to delete the entire my_project tree:

python
import shutil
from pathlib import Path

# Step 7: Remove the entire project tree (permanent — no undo)
target = Path("my_project")
if target.is_dir():
    shutil.rmtree(target)
    print(f"Deleted {target} and all its contents.")
else:
    print(f"{target} does not exist — nothing to delete.")

Expected output:

Deleted my_project and all its contents.
What you just learned

In this exercise you used Path.mkdir(parents=True, exist_ok=True) to build a full nested tree, Path.rglob() to list and verify everything recursively, Path.write_text() to create a file, and shutil.rmtree() to clean up safely. These are the four operations you will use together in real Python projects.

Real-World Directory Patterns You Won't Find Everywhere

The three core functions cover most cases, but production Python code regularly needs patterns beyond a bare mkdir call. Here are the ones that distinguish scripts that work reliably from scripts that fail in the field.

Temporary directories that clean themselves up

When your code needs a scratch space that must not persist after the script exits — for intermediate build artifacts, test fixtures, or untrusted data — use tempfile.TemporaryDirectory from the standard library. It creates a uniquely named directory in the system's temp location and deletes it automatically when the context manager exits, even if an exception is raised.

python
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    tmp_path = Path(tmp)
    # Do work inside the temp directory
    work_file = tmp_path / "intermediate.txt"
    work_file.write_text("processing...", encoding="utf-8")
    print(f"Working in: {tmp_path}")
    # tmp_path is automatically deleted when the 'with' block exits
    # even if an exception occurs inside it

print("Temp directory has been removed.")
Why this matters

Using tempfile.TemporaryDirectory instead of a manually created temp path prevents disk clutter when scripts are interrupted. It also avoids name collisions: the system guarantees the generated name is unique, so two simultaneous script runs cannot accidentally write into the same directory.

Ensuring a project directory tree exists at startup

Scripts that write output files often need to guarantee a set of directories exists before they begin. The pattern below is idiomatic in CLI tools, data pipelines, and test setups. Calling it once at startup is safer than scattering mkdir calls throughout the code, because any permission failure surfaces early and clearly rather than mid-run.

python
from pathlib import Path

# Define your project layout as a list
REQUIRED_DIRS = [
    "output/reports",
    "output/logs",
    "output/tmp",
    "data/raw",
    "data/processed",
]

def init_workspace(base: Path = Path(".")) -> None:
    """Create all required directories if they don't already exist."""
    for rel in REQUIRED_DIRS:
        target = base / rel
        try:
            target.mkdir(parents=True, exist_ok=True)
        except PermissionError as e:
            raise RuntimeError(f"Cannot create required directory {target}: {e}") from e
    print(f"Workspace initialized under {base.resolve()}")

init_workspace()
spot the bug click the line that contains the error

This workspace initializer runs in a CI pipeline. It creates the directories but an exception is silently swallowed on the wrong line, meaning a permission failure will go undetected. Which line is wrong?

1from pathlib import Path
2
3DIRS = ["output/reports", "output/logs", "data/raw"]
4
5def init_workspace(base=Path(".")):
6 for rel in DIRS:
7 try:
8 (base / rel).mkdir(parents=True, exist_ok=True)
9 except Exception:
10 pass
Why: except Exception: pass silently swallows every error including PermissionError. If the CI runner lacks write permission to a required directory, the function will return successfully with some directories never created, and the failure won't surface until later in the pipeline — often in a confusing unrelated error. The fix is to catch specific exceptions and re-raise or log them: except PermissionError as e: raise RuntimeError(f"Cannot create {base / rel}: {e}") from e. Bare except Exception: pass in infrastructure code is almost always a bug.

Atomic directory creation: detecting a truly new directory

There is one scenario where you specifically do not want exist_ok=True: when your application's logic depends on knowing whether it was the process that created the directory, as opposed to finding a directory that already existed. This comes up in distributed systems, job schedulers, and file-based locking. Without exist_ok=True, os.mkdir() raises FileExistsError only when the directory already exists — making it a single-call ownership signal.

python
import os

JOB_LOCK_DIR = "/tmp/myjob.lock"

try:
    os.mkdir(JOB_LOCK_DIR)
    # Only this process reaches here — it "owns" the lock
    print("Lock acquired. Running exclusive job...")
    # ... do exclusive work ...
except FileExistsError:
    print("Another process is already running. Exiting.")
finally:
    # Clean up the lock directory when done
    try:
        os.rmdir(JOB_LOCK_DIR)
    except OSError:
        pass  # Already removed or never created
"The os module provides a portable way of using operating system dependent functionality." — Python Software Foundation, os module documentation

This directory-as-lock pattern works because on all POSIX-compliant systems and on modern Windows, mkdir is atomic at the kernel level: exactly one process succeeds, and all others get an error. It is not a replacement for proper inter-process locking libraries in complex systems, but for simple scripts it is a well-understood and dependency-free approach.

Getting the directory a script lives in

A frequent beginner stumbling block is creating a directory relative to the wrong path. When a Python script runs, its working directory is wherever the terminal was when the script was invoked — not where the script file lives. To create a directory next to the script file itself, resolve the script's own location using __file__.

python
from pathlib import Path

# Path to the directory this script lives in
script_dir = Path(__file__).parent.resolve()

# Create an "output" folder next to the script, not next to wherever
# the terminal happens to be when the script runs
output_dir = script_dir / "output"
output_dir.mkdir(exist_ok=True)

print(f"Output will be written to: {output_dir}")
Why .resolve()?

Calling .resolve() on the path converts it to an absolute path and resolves any symlinks in the chain. Without it, __file__ may be a relative path in some execution environments, which can produce different-looking results depending on how the script was called. Using .resolve() eliminates that ambiguity.

concept map click any concept to see how it connects to the others

Every concept in this article connects to at least two others. This map shows the web of relationships. Click a node to highlight its connections and see a brief explanation.

Select a concept above to see its connections explained.
misconception buster click each card to see why the belief is wrong

These are the five beliefs that produce the most confusion and bugs in real Python code. Each one is widely repeated. All five are wrong.

believe this?

"mode=0o777 means the directory will be world-readable and world-writable."

tap to find out
not quite

0o777 is the requested mode, but the kernel applies mode & ~umask before creating the directory. With a typical umask of 0o022, the actual permissions become 0o755 — not 0o777. To get world-writable permissions you would need to either set umask to 0 first, or call os.chmod() afterward. The mode argument is a ceiling, not a guarantee.

believe this?

"exist_ok=True suppresses all errors from os.makedirs()."

tap to find out
false

exist_ok=True suppresses exactly one error: FileExistsError when the target already exists as a directory. It does not suppress PermissionError (no write access), OSError when a file occupies the path, FileExistsError from a broken symlink at the target, or any other OS-level failure. The call still raises on any error condition other than "the directory already exists."

believe this?

"Path.mkdir() and os.makedirs() are equivalent in every way."

tap to find out
almost, but not quite

They differ in one non-obvious way: since Python 3.7, os.makedirs() only applies the mode argument to the leaf directory. Before 3.7, it applied to all newly created directories. Path.mkdir(parents=True) calls os.mkdir() directly for each level without the mode distinction that os.makedirs() has. For most use cases they behave identically, but when setting non-default permissions on intermediate directories the difference matters.

believe this?

"Checking os.path.isdir() before mkdir() is safer than using exist_ok=True."

tap to find out
backwards

The check-then-act pattern (if not isdir(): mkdir()) introduces a TOCTOU race condition. Between your check and your creation call, another process or thread can create the same directory — and then your mkdir() fails with FileExistsError. exist_ok=True is safer precisely because it delegates the check and the creation to a single atomic kernel call: exactly one process succeeds; the others get a suppressed error or an appropriate exception.

believe this?

"os.path.exists() is the right way to check if a directory exists before using it."

tap to find out
wrong tool

os.path.exists() returns True for files, directories, symlinks, named pipes, and sockets — anything at that path. If a file named output blocks your intended directory, exists() returns True and your code skips creation, then fails later with a confusing error when it tries to write into what it believes is a directory. Use os.path.isdir() or Path.is_dir() when you specifically need a directory. Better still, skip the check entirely and let exist_ok=True handle it — if the path is a file, the OSError is immediate and unambiguous.

Python Learning Summary Points

  1. os.mkdir() creates a single directory and requires all parent directories to exist already. It raises FileExistsError if the directory is already present and FileNotFoundError if a parent is missing.
  2. os.makedirs() creates a full directory path including any missing intermediate directories — equivalent to mkdir -p. Passing exist_ok=True makes the call safe to run repeatedly without raising an error. The exist_ok parameter was added in Python 3.2.
  3. pathlib's Path.mkdir(parents=True, exist_ok=True) is the modern, object-oriented equivalent. It is preferred in contemporary Python code because Path objects integrate naturally with the rest of the pathlib API. Under the hood in CPython, Path.mkdir() calls os.mkdir() directly. The exist_ok parameter on Path.mkdir() was added in Python 3.5.
  4. The mode parameter sets Unix permission bits, but the final permissions are mode & ~umask — a bitwise AND with the umask complement, not plain subtraction. If the parent directory has a default ACL set, the umask is bypassed entirely and the ACL is inherited instead. On Windows, mode has no effect except for 0o700 handling, backported to Python 3.8 through 3.12 as a fix for CVE-2024-4030. Since Python 3.7, the mode argument in os.makedirs() only applies to the leaf directory — intermediate directories use the process default. Before Python 3.4.1, passing exist_ok=True to makedirs() would still raise an error if the existing directory's mode did not match the requested mode; this behavior was removed in 3.4.1 (bpo-21082) because it could not be implemented safely.
  5. Always handle PermissionError separately. A missing directory is recoverable; a permissions failure requires a different response. Avoid passing paths containing .. to os.makedirs() — the official documentation explicitly warns that the function will become confused by parent-directory traversal components; normalize first with Path.resolve().
  6. Using exist_ok=True is safer than checking with is_dir() first, because the check-then-create pattern introduces a TOCTOU race condition. exist_ok=True makes the operation atomic from Python's perspective.
  7. For temporary scratch space, use tempfile.TemporaryDirectory as a context manager — it guarantees cleanup on exit even when exceptions occur. For directories that must persist, use Path(__file__).parent.resolve() to anchor paths to the script location rather than the terminal's working directory.
  8. Relative paths resolve from the current working directory — use os.getcwd() or Path.cwd() to see where that is. Absolute paths resolve the same way every time. Always use os.path.isdir() or Path.is_dir() rather than os.path.exists() when checking for a directory specifically.
  9. To delete a directory, use os.rmdir() or Path.rmdir() for empty directories, and shutil.rmtree() for recursive deletion. To rename, use Path.rename() or os.rename(). To move across filesystems, use shutil.move(). Use pathlib's / operator or os.path.join() to build paths — never hardcode separators.

Knowing when to use os.mkdir() versus os.makedirs() versus Path.mkdir() comes down to one question: do you need to create intermediate directories, and are you working in a code base that already uses pathlib for path handling? If intermediate directories may not exist, always choose the recursive option and always pass exist_ok=True unless you specifically need to detect an existing directory as an error condition.

check your understanding question 1 of 5

Frequently Asked Questions

os.mkdir() creates a single directory and raises FileNotFoundError if any parent directory in the path does not exist. os.makedirs() creates the target directory and any missing parent directories in the path at once, similar to the shell command mkdir -p.

Pass exist_ok=True to either os.makedirs() or pathlib.Path.mkdir(). For example: os.makedirs('my_folder', exist_ok=True) or Path('my_folder').mkdir(exist_ok=True). Both suppress FileExistsError when the directory is already present.

Use os.makedirs('parent/child/grandchild', exist_ok=True) or Path('parent/child/grandchild').mkdir(parents=True, exist_ok=True). Both approaches create all intermediate directories in a single call.

pathlib is a standard library module introduced in Python 3.4 that provides an object-oriented interface for file system paths. It is generally preferred in modern Python code because Path objects are more readable and composable than string-based paths. For new projects, Path.mkdir() with parents=True and exist_ok=True is the recommended approach.

Python raises FileExistsError (a subclass of OSError) when you call os.mkdir() or os.makedirs() on a path that already exists and exist_ok is False (the default).

You can use os.path.isdir('my_folder') from the os module, or Path('my_folder').is_dir() from pathlib. Both return True if the path exists and is a directory. However, using exist_ok=True is a safer pattern than checking first, because checking then creating introduces a TOCTOU (time-of-check to time-of-use) race condition: another process could create or delete the directory between your check and your creation call. Using exist_ok=True makes the entire operation atomic from Python's perspective.

Not always exactly. On Unix-like systems, the final permissions are calculated as mode & ~umask. For example, if you pass mode=0o777 and your process's umask is 0o022, the resulting directory will have 0o755 permissions, not 0o777. On Windows, the mode parameter has no effect except for 0o700 handling, which was backported to Python 3.8 through 3.12 as a security fix for CVE-2024-4030, restricting the new directory to the current user via ACLs. If exact permissions are critical on Unix, set the umask explicitly before creating the directory and restore it immediately after.

The Python documentation explicitly warns that os.makedirs() will become confused if any path element contains .. (the parent-directory reference). The function may raise unexpected errors or create directories in unintended locations. If your path could contain .. — for example, if it comes from user input or is assembled from dynamic parts — normalize it first with os.path.abspath(path) or Path(path).resolve() before passing it to any directory creation function.

When parents=True is passed to Path.mkdir(), Python creates any missing parent directories needed to form the full path. This is equivalent to os.makedirs() and to the shell command mkdir -p. Without parents=True, FileNotFoundError is raised if any parent directory is missing.

You can create a directory at any path your Python process has permission to write to. Attempting to create a directory in a restricted location raises PermissionError. Always wrap directory creation in a try-except block to handle permission issues gracefully, especially in scripts that run in shared or production environments.

Use Path(__file__).parent.resolve() to get the absolute path of the directory containing your script, then build your target from that: output = Path(__file__).parent.resolve() / "output". Without anchoring to __file__, directory creation is relative to wherever the terminal was when you ran the script, which varies depending on how and from where it is invoked.

Use tempfile.TemporaryDirectory as a context manager whenever you need a scratch directory that must be deleted when your code finishes — including when exceptions occur. It creates a uniquely named directory in the system temp location and handles deletion automatically. Use os.mkdir() or Path.mkdir() when the directory needs to persist after the script exits.

FileNotFoundError when creating a directory means the parent path does not exist. For example, calling os.mkdir("a/b/c") when neither a nor a/b exists will raise FileNotFoundError because os.mkdir() only creates the final directory in the path — it will not create intermediate parents. The fix is to use os.makedirs("a/b/c", exist_ok=True) or Path("a/b/c").mkdir(parents=True, exist_ok=True), both of which create the full chain.

os.path.exists() returns True for any path that exists — whether it is a file, a directory, a symlink, or anything else. os.path.isdir() returns True only when the path exists and is specifically a directory. When your code depends on a path being a directory, always use os.path.isdir() or Path.is_dir(). Using exists() here can cause silent failures if a file happens to have the same name as the directory you expected.

The current working directory (cwd) is the directory Python resolves relative paths from. It is wherever your terminal was when you ran the script — not necessarily where the script file lives on disk. Use os.getcwd() or Path.cwd() to see it. If your directories keep appearing in the wrong place, print the cwd at the top of your script as a quick diagnostic, then use Path(__file__).parent.resolve() to anchor paths to the script's own location instead.

Yes. Python treats spaces in directory names as ordinary characters. os.makedirs("my project/output files", exist_ok=True) works exactly as you would expect. The only place spaces require extra care is in shell commands, where you would need to quote the path. Inside Python code itself, no escaping or special handling is needed.

Use os.rmdir() or Path.rmdir() to delete a single empty directory — both raise OSError if any content remains inside. To delete a directory and all its contents recursively, use shutil.rmtree(path). This operation is permanent and immediate — nothing goes to a trash folder — so always verify the path before calling it.

To rename a directory within the same parent, use Path("old_name").rename("new_name") or os.rename("old_name", "new_name"). To move a directory to a different location — including across different drives or mount points — use shutil.move(src, dst), which handles cross-filesystem moves that os.rename() cannot.

The functions themselves work the same way across all three platforms. The main differences are: path separators (\ on Windows, / on Unix — use pathlib's / operator or os.path.join() to avoid hardcoding either); the mode parameter for permissions has no effect on Windows except for 0o700 handling backported to Python 3.8 through 3.12 (CVE-2024-4030); and PermissionError conditions reflect the underlying OS permissions model, which differs between platforms. Using pathlib abstracts away separator differences automatically.

Yes — there are three version-specific behavior changes worth knowing. First, exist_ok was added to os.makedirs() in Python 3.2; it did not exist in Python 2 or Python 3.0/3.1. Second, before Python 3.4.1, passing exist_ok=True would still raise an error if the existing directory's permission mode did not match the mode argument — this behavior was removed as impossible to implement safely (bpo-21082). Third, starting in Python 3.7, the mode argument stopped applying to intermediate parent directories created by os.makedirs(); it now applies only to the leaf directory. Before 3.7, all newly created directories in the path received the specified mode. Code that relied on setting specific permissions on intermediate directories via the mode argument started producing different results after the 3.7 upgrade without any deprecation warning.

Certificate of Completion
Final Exam
Pass mark: 80%|Score 80% or higher to receive your certificate

Enter your name as you want it to appear on your certificate, then start the exam. Your name is used only to generate your certificate and is never transmitted or stored anywhere.

Question 1 of 10

Sources

Every technical claim in this article is verifiable against the following primary sources: