ZetCode

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.

Taskpathlibos / shutil
createPath.mkdiros.mkdir, os.makedirs
renamePath.renameos.rename
remove emptyPath.rmdiros.rmdir
remove tree-shutil.rmtree
copy treePath.copyshutil.copytree
movePath.move, Path.move_intoshutil.move
listPath.iterdir, Path.globos.listdir, os.scandir
walkPath.walkos.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.

create_dir.py
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.

create_dir2.py
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.

create_nested.py
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.

parent_mode.py
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.

temp_dir.py
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.

rename_dir.py
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.

remove_dir.py
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.

copy_dir.py
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.

move_dir.py
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.

current_dir.py
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.

home_dir.py
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.

parents.py
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.

list_dir.py
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.

list_dirs.py
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.

recursive.py
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.

walk_dir.py
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.

dir_size.py
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.

check_dir.py
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.