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.
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.
Both os and pathlib ship with Python — no third-party installation is required. You get them with any standard Python 3 install.
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.
import os
# Create a single directory named "output" in the current working directory
os.mkdir("output")
print("Directory created.")
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.
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.
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.
The code below tries to create a directory and print a message on success, but it has one error. Which line is wrong?
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.
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.
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.
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().
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.
os.mkdir("data/reports/2026") — parent directories do not exist yet. Watch what happens.
Build the correct call to create nested directories safely, suppressing the error if they already exist:
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.
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.
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?
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.
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)
Build the correct pathlib call to create logs/archive/2026 — creating all missing parents — and suppress any error if the directory already exists:
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.
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.
When you write Path("output").mkdir(), Python executes a chain of calls that ends at the operating system kernel. Each layer below is collapsible.
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.
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.
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)
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.
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.
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.
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)
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.
Enter a working directory and a path argument. The tool resolves where Python would actually create the directory.
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.
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.
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
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.
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.
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
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?
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.
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.
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.
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() 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.
The directory data/ exists and contains one file. You run: os.rmdir("data"). What happens? Then you run os.removedirs("data"). What happens differently?
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.
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
Pick a situation. The simulator shows the code, the exact traceback Python would produce, and the explanation of what went wrong.
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.
-
Import the required module
At the top of your script, write
import osto use the os module, orfrom pathlib import Pathto use pathlib. Both modules are part of Python's standard library — no installation required. -
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. -
Call the directory creation function
For a single directory whose parent already exists, use
os.mkdir("folder")orPath("folder").mkdir(). For nested directories, useos.makedirs("parent/child", exist_ok=True)orPath("parent/child").mkdir(parents=True, exist_ok=True). -
Add error handling
If you are not using
exist_ok=True, wrap the call in atry-exceptblock that catches at minimumFileExistsErrorandPermissionError. In automated scripts and pipelines,exist_ok=Trueis almost always the right choice because it makes the operation idempotent — safe to run more than once. -
Verify the directory was created
Confirm the result with
os.path.isdir("folder")orPath("folder").is_dir(). Both returnTrueif 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:
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.
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:
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:
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:
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
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
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:
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.
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.
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.")
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.
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()
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?
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.
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__.
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}")
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.
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.
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.
"mode=0o777 means the directory will be world-readable and world-writable."
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.
"exist_ok=True suppresses all errors from os.makedirs()."
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."
"Path.mkdir() and os.makedirs() are equivalent in every way."
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.
"Checking os.path.isdir() before mkdir() is safer than using exist_ok=True."
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.
"os.path.exists() is the right way to check if a directory exists before using it."
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
os.mkdir()creates a single directory and requires all parent directories to exist already. It raisesFileExistsErrorif the directory is already present andFileNotFoundErrorif a parent is missing.os.makedirs()creates a full directory path including any missing intermediate directories — equivalent tomkdir -p. Passingexist_ok=Truemakes the call safe to run repeatedly without raising an error. Theexist_okparameter was added in Python 3.2.- pathlib's
Path.mkdir(parents=True, exist_ok=True)is the modern, object-oriented equivalent. It is preferred in contemporary Python code becausePathobjects integrate naturally with the rest of the pathlib API. Under the hood in CPython,Path.mkdir()callsos.mkdir()directly. Theexist_okparameter onPath.mkdir()was added in Python 3.5. - The
modeparameter sets Unix permission bits, but the final permissions aremode & ~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,modehas no effect except for0o700handling, backported to Python 3.8 through 3.12 as a fix for CVE-2024-4030. Since Python 3.7, themodeargument inos.makedirs()only applies to the leaf directory — intermediate directories use the process default. Before Python 3.4.1, passingexist_ok=Truetomakedirs()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. - Always handle
PermissionErrorseparately. A missing directory is recoverable; a permissions failure requires a different response. Avoid passing paths containing..toos.makedirs()— the official documentation explicitly warns that the function will become confused by parent-directory traversal components; normalize first withPath.resolve(). - Using
exist_ok=Trueis safer than checking withis_dir()first, because the check-then-create pattern introduces a TOCTOU race condition.exist_ok=Truemakes the operation atomic from Python's perspective. - For temporary scratch space, use
tempfile.TemporaryDirectoryas a context manager — it guarantees cleanup on exit even when exceptions occur. For directories that must persist, usePath(__file__).parent.resolve()to anchor paths to the script location rather than the terminal's working directory. - Relative paths resolve from the current working directory — use
os.getcwd()orPath.cwd()to see where that is. Absolute paths resolve the same way every time. Always useos.path.isdir()orPath.is_dir()rather thanos.path.exists()when checking for a directory specifically. - To delete a directory, use
os.rmdir()orPath.rmdir()for empty directories, andshutil.rmtree()for recursive deletion. To rename, usePath.rename()oros.rename(). To move across filesystems, useshutil.move(). Use pathlib's/operator oros.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.
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.
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.
Sources
Every technical claim in this article is verifiable against the following primary sources:
- Python Software Foundation. os — Miscellaneous operating system interfaces. Python 3 documentation. docs.python.org/3/library/os.html
- Python Software Foundation. pathlib — Object-oriented filesystem paths. Python 3 documentation. docs.python.org/3/library/pathlib.html
- Pitrou, Antoine. PEP 428 — The pathlib module — object-oriented filesystem paths. Python Software Foundation, 2012. peps.python.org/pep-0428/
- Python Software Foundation. What's New In Python 3.4. Python documentation. docs.python.org/dev/whatsnew/3.4.html
- CPython project. Lib/pathlib.py. GitHub. github.com/python/cpython
- Python Software Foundation. tempfile — Generate temporary files and directories. Python 3 documentation. docs.python.org/3/library/tempfile.html
- Python Software Foundation. shutil — High-level file operations. Python 3 documentation. docs.python.org/3/library/shutil.html