Python directory
last modified October 30, 2026 Python 3.15
In this article we show how to work with directories in Python. We create, rename, copy, move, remove, and list directories.
Directory definition
A directory is an organizing unit in a computer's file system for storing and locating files. Directories are hierarchically organized into a tree of directories. Directories have parent-child relationships. A directory is sometimes also called a folder.
Python has functions for manipulating directories in the os,
shutil, and pathlib modules. In this article, we
mostly use the pathlib module, which has the more modern
object-oriented API.
| Task | pathlib | os / shutil |
|---|---|---|
| create | Path.mkdir | os.mkdir, os.makedirs |
| rename | Path.rename | os.rename |
| remove empty | Path.rmdir | os.rmdir |
| remove tree | - | shutil.rmtree |
| copy tree | Path.copy | shutil.copytree |
| move | Path.move, Path.move_into | shutil.move |
| list | Path.iterdir, Path.glob | os.listdir, os.scandir |
| walk | Path.walk | os.walk |
Path.copy, Path.move, and
Path.move_into were added in Python 3.14, Path.walk
in Python 3.12.
Creating a directory
The Path.mkdir method creates a directory. If the directory
already exists, a FileExistsError is raised.
from pathlib import Path
p = Path('test')
p.mkdir()
The example creates the test directory in the current working
directory.
$ python create_dir.py
$ ls -d test
test
$ python create_dir.py
Traceback (most recent call last):
File "/home/user/python/create_dir.py", line 4, in <module>
p.mkdir()
~~~~~~~^^
File "/usr/lib/python3.15/pathlib/__init__.py", line 1210, in mkdir
os.mkdir(self, mode)
~~~~~~~~^^^^^^^^^^^^
FileExistsError: [Errno 17] File exists: 'test'
Ignoring existing directories
When we run the previous program twice, an exception is raised. If the
exist_ok flag is set to True, the
FileExistsError is ignored.
from pathlib import Path
p = Path('test')
p.mkdir(exist_ok=True)
print(f'{p} exists: {p.is_dir()}')
The test directory exists already; the program runs without
errors. If the path exists but is not a directory, the error is still
raised.
Checking with exists before calling mkdir is not
a good alternative: another process can create the directory between the
check and the call.
$ python create_dir2.py test exists: True
Creating nested directories
With parents=True, mkdir also creates the missing
parent directories, like the mkdir -p shell command.
from pathlib import Path
p = Path('reports/2026/10')
p.mkdir(parents=True, exist_ok=True)
for d in sorted(Path('reports').rglob('*')):
print(d)
The example creates the reports/2026/10 path. Without
parents=True, a FileNotFoundError would be raised,
because the reports directory does not exist.
$ python create_nested.py reports/2026 reports/2026/10
Permissions of parent directories
The mode argument applies only to the last directory of the
path; the parents created with parents=True get the default
mode. Python 3.15 added the parent_mode keyword argument to
Path.mkdir and os.makedirs, which sets the mode of
the created parents.
from pathlib import Path
Path('private/keys/ssh').mkdir(mode=0o700, parents=True)
Path('shared/keys/ssh').mkdir(mode=0o700, parents=True, parent_mode=0o750)
We create two paths with a private last directory. In the second call, the
parents get the 0o750 mode. Both modes are modified by the
process umask, which is 022 here.
$ python parent_mode.py $ stat -c '%A %n' private private/keys private/keys/ssh drwxr-xr-x private drwxr-xr-x private/keys drwx------ private/keys/ssh $ stat -c '%A %n' shared shared/keys shared/keys/ssh drwxr-x--- shared drwxr-x--- shared/keys drwx------ shared/keys/ssh
Creating a temporary directory
The tempfile.TemporaryDirectory function securely creates a
temporary directory. When the context manager exits, the directory and all
its contents are removed.
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory(prefix='build-') as tempdir:
print(f'created temporary directory {tempdir}')
p = Path(tempdir) / 'output.txt'
p.write_text('temporary data')
print(f'{p.name} exists: {p.exists()}')
print(f'directory exists: {Path(tempdir).exists()}')
We create a temporary directory with a name prefix and write a file into it.
After the with block, the directory no longer exists.
The directory is created in the system's temporary directory, which is
/tmp on Linux. The name is random, so it differs on each run.
$ python temp_dir.py created temporary directory /tmp/build-3pud1rhl output.txt exists: True directory exists: False
Renaming a directory
The Path.rename method renames a directory and returns the new
path.
from pathlib import Path
src = Path('test')
dest = src.rename('test2')
print(f'{src} renamed to {dest}')
print(src.exists(), dest.exists())
We rename the test directory to test2. The
original Path object still refers to the old path, which does
not exist anymore.
$ python rename_dir.py test renamed to test2 False True
Removing a directory
A directory is removed with the Path.rmdir method. The
directory must be empty. To remove a directory with its contents, we use
shutil.rmtree.
import shutil
from pathlib import Path
Path('empty').mkdir()
Path('empty').rmdir()
print('removed empty directory')
full = Path('full')
full.mkdir()
(full / 'data.txt').write_text('some data')
try:
full.rmdir()
except OSError as e:
print(e)
shutil.rmtree(full)
print(f'{full} exists: {full.exists()}')
Removing a non-empty directory with rmdir raises an
OSError. The rmtree function deletes the whole
tree; it should be used with care, since the files do not go to a trash
bin.
$ python remove_dir.py removed empty directory [Errno 39] Directory not empty: 'full' full exists: False
Copying a directory
The Path.copy method copies a file or a whole directory tree.
The shutil.copytree function does the same and supports
filtering.
import shutil
from pathlib import Path
src = Path('projects')
dst = src.copy('projects-copy')
print(f'copied to {dst}')
shutil.copytree(src, 'projects-src', ignore=shutil.ignore_patterns('*.csv', '*.sh'))
We copy the projects directory twice. The second copy leaves out
the CSV and shell files; the ignore_patterns function creates a
filter from glob patterns.
$ python copy_dir.py copied to projects-copy $ find projects-copy -type f | sort projects-copy/README.md projects-copy/blog/app.py projects-copy/blog/templates/index.html projects-copy/data/users.csv projects-copy/notes.txt projects-copy/scripts/backup.py projects-copy/scripts/clean.sh $ find projects-src -type f | sort projects-src/README.md projects-src/blog/app.py projects-src/blog/templates/index.html projects-src/notes.txt projects-src/scripts/backup.py
Moving a directory
The shutil.move function recursively moves a file or directory
to another location and returns the destination. If the destination is on
another file system, the files are copied and the source is removed.
import shutil
from pathlib import Path
dest = shutil.move('docs', 'docs2')
print(dest)
archive = Path('archive')
archive.mkdir()
moved = Path('docs2').move_into(archive)
print(moved)
We first move the docs directory to docs2. Then we
move docs2 into the archive directory with the
Path.move_into method, which keeps the name of the moved
directory.
$ mkdir docs $ touch docs/data.txt docs/names.csv docs/numbers.csv $ python move_dir.py docs2 archive/docs2 $ ls archive/docs2 data.txt names.csv numbers.csv
Current working directory
The Path.cwd method returns a new path object representing the
current working directory. The os.chdir function changes it.
import contextlib
import os
from pathlib import Path
print(f'Current working directory: {Path.cwd()}')
os.chdir('projects')
print(f'Current working directory: {Path.cwd()}')
os.chdir('..')
with contextlib.chdir('projects/blog'):
print(f'Inside the block: {Path.cwd()}')
print(Path('app.py').exists())
print(f'After the block: {Path.cwd()}')
We change the working directory with os.chdir and back again.
The contextlib.chdir context manager changes the directory for
the duration of a block and restores the original afterwards. Relative paths
are resolved against the current directory, so app.py is found
inside the block.
$ python current_dir.py Current working directory: /home/user/python Current working directory: /home/user/python/projects Inside the block: /home/user/python/projects/blog True After the block: /home/user/python
Home directory
The Path.home method returns a new path object representing the
user's home directory.
from pathlib import Path
print(Path.home())
print(Path('~/Documents/notes.txt').expanduser())
print(Path.home() / '.config')
The expanduser method replaces the leading ~ with
the home directory. The / operator joins paths.
$ python home_dir.py /home/user /home/user/Documents/notes.txt /home/user/.config
Directory parents
The Path.parent attribute returns the parent of the path and
Path.parents a sequence of its ancestors.
from pathlib import Path
path = Path('/home/user/python/projects/blog/templates')
print(f'The parent directory of {path} is {path.parent}')
print(f'The parent of the parent is {path.parent.parent}')
print(f'The third ancestor is {path.parents[2]}')
print('All the ancestors:')
for parent in path.parents:
print(parent)
print(path.name, path.parts)
The ancestors can be accessed by index; parents[0] is the
parent. The name attribute returns the last component and
parts all components. These are pure path operations; the
directories do not need to exist.
$ python parents.py
The parent directory of /home/user/python/projects/blog/templates is /home/user/python/projects/blog
The parent of the parent is /home/user/python/projects
The third ancestor is /home/user/python
All the ancestors:
/home/user/python/projects/blog
/home/user/python/projects
/home/user/python
/home/user
/home
/
templates ('/', 'home', 'user', 'python', 'projects', 'blog', 'templates')
Listing directory contents
The Path.iterdir method yields path objects of the directory
contents. The children are yielded in arbitrary order, and the special
entries . and .. are not included.
from pathlib import Path
path = Path('projects')
for entry in sorted(path.iterdir()):
kind = 'dir ' if entry.is_dir() else 'file'
print(kind, entry.name)
The example lists the files and directories of the projects
directory. The listing is not recursive. We sort the entries to get a
stable order.
$ python list_dir.py file README.md dir blog dir data file notes.txt dir scripts
Checking if a path is a directory
The Path.is_dir method returns True if the path
points to a directory; is_file checks for a regular file.
from pathlib import Path
path = Path('projects')
dirs = [e.name for e in path.iterdir() if e.is_dir()]
files = [e.name for e in path.iterdir() if e.is_file()]
print('directories:', *sorted(dirs))
print('files:', *sorted(files))
We separate the immediate subdirectories of projects from its
files.
$ python list_dirs.py directories: blog data scripts files: README.md notes.txt
Listing a directory recursively
The Path.glob method yields the paths matching a pattern, and
Path.rglob matches the pattern recursively in all
subdirectories.
from pathlib import Path
path = Path('projects')
print('Python files:')
for e in sorted(path.rglob('*.py')):
print(e)
print('Top-level text files:')
for e in sorted(path.glob('*.txt')):
print(e)
print('Files one level down:')
for e in sorted(path.glob('*/*.*')):
print(e)
We find all Python files in the projects tree, the text files
directly in projects, and the files with an extension one
level down. The * wildcard does not cross directory
boundaries.
$ python recursive.py Python files: projects/blog/app.py projects/scripts/backup.py Top-level text files: projects/notes.txt Files one level down: projects/blog/app.py projects/data/users.csv projects/scripts/backup.py projects/scripts/clean.sh
Walking a directory tree
The Path.walk method walks a directory tree top-down. For each
directory, it yields the directory path and the names of its subdirectories
and files.
from pathlib import Path
for root, dirs, files in Path('projects').walk():
dirs.sort()
depth = len(root.parts) - 1
print(f'{" " * depth}{root.name}/')
for name in sorted(files):
size = (root / name).stat().st_size
print(f'{" " * (depth + 1)}{name} ({size} B)')
We print the tree with file sizes. Sorting the dirs list in
place changes the order in which walk visits the
subdirectories; removing names from it would skip them.
$ python walk_dir.py
projects/
README.md (35 B)
notes.txt (19 B)
blog/
app.py (100 B)
templates/
index.html (60 B)
data/
users.csv (44 B)
scripts/
backup.py (60 B)
clean.sh (31 B)
Directory size
The os.scandir function returns DirEntry objects,
which cache the file type information. It is the fastest way to traverse
large trees.
import os
from pathlib import Path
def dir_size(path):
total = 0
with os.scandir(path) as entries:
for entry in entries:
if entry.is_file(follow_symlinks=False):
total += entry.stat(follow_symlinks=False).st_size
elif entry.is_dir(follow_symlinks=False):
total += dir_size(entry.path)
return total
for d in sorted(Path('projects').iterdir()):
if d.is_dir():
print(f'{d.name:10} {dir_size(d):5} B')
print(f'{"total":10} {dir_size("projects"):5} B')
files = [p for p in Path('projects').rglob('*') if p.is_file()]
print(f'{"rglob":10} {sum(p.stat().st_size for p in files):5} B')
The recursive dir_size function sums the sizes of all files in
a directory. With follow_symlinks=False, symbolic links are not
followed, so a link cannot cause an endless loop. The last line computes the
same total with rglob.
$ python dir_size.py blog 160 B data 44 B scripts 91 B total 349 B rglob 349 B
Checking paths
The exists, is_dir, and is_file
methods check what a path points to. They return False for
missing paths instead of raising an exception.
from pathlib import Path
for name in ['projects', 'projects/notes.txt', 'missing']:
p = Path(name)
print(f'{name:20} exists={p.exists()!s:5} is_dir={p.is_dir()!s:5} '
f'is_file={p.is_file()}')
empty = Path('empty')
empty.mkdir()
print(f'{empty} is empty: {not any(empty.iterdir())}')
print(f'projects is empty: {not any(Path("projects").iterdir())}')
To check if a directory is empty, we ask iterdir for its first
entry with any; this does not read the whole listing.
$ python check_dir.py projects exists=True is_dir=True is_file=False projects/notes.txt exists=True is_dir=False is_file=True missing exists=False is_dir=False is_file=False empty is empty: True projects is empty: False
Source
pathlib - Object-oriented filesystem paths - Python documentation
In this article we have worked with directories in Python.
Author
List all Python tutorials.