Skip to content
Happy Programming Guide
Start learning
Python

Python File Moving Using shutil

How shutil.move works, when it differs from os.rename, and how to move files safely across drives without overwriting anything by accident.

A person working on a laptop

shutil.move() moves a file or folder from one place to another, and unlike os.rename() it works across drives and filesystems. That is the main reason to prefer it. This guide covers how it behaves in each situation, the overwrite trap, and how to write a move that cannot destroy data.

The basics#

Python
import shutil

# Move into an existing folder, keeping the name
shutil.move("report.pdf", "archive/")

# Move and rename in one step
shutil.move("report.pdf", "archive/report-2026.pdf")

# Move a whole folder
shutil.move("temp/", "backup/temp/")

The function returns the new path, which is worth keeping:

Python
new_path = shutil.move("report.pdf", "archive/")
print(new_path)     # archive/report.pdf

How the destination is interpreted#

This is where the surprises come from:

Destination What happens
Existing folder Moved into it, name unchanged
Path that does not exist Treated as the new file name
Existing file Overwritten without warning
Folder that does not exist Treated as a file name, no folder created

That last row catches people out. shutil.move("a.txt", "newfolder/") when newfolder does not exist fails with an error rather than creating it. Create the folder first:

Python
from pathlib import Path

Path("archive").mkdir(parents=True, exist_ok=True)
shutil.move("report.pdf", "archive/")

The overwrite trap#

Python
shutil.move("draft.txt", "final.txt")     # destroys final.txt silently

No exception, no warning, no way back. If there is any chance the destination exists, check first:

Python
from pathlib import Path
import shutil


def safe_move(source, destination):
    """Move without ever overwriting: adds -1, -2 and so on."""
    source = Path(source)
    destination = Path(destination)

    if destination.is_dir():
        destination = destination / source.name

    if destination.exists():
        stem, suffix = destination.stem, destination.suffix
        counter = 1
        while destination.exists():
            destination = destination.with_name(f"{stem}-{counter}{suffix}")
            counter += 1

    destination.parent.mkdir(parents=True, exist_ok=True)
    return shutil.move(str(source), str(destination))


print(safe_move("report.pdf", "archive/"))

Using .stem and .suffix rather than splitting on a dot means archive.tar.gz becomes archive.tar-1.gz instead of something broken.

shutil.move versus os.rename#

Python
import os

os.rename("a.txt", "b.txt")                    # same filesystem only
os.rename("C:/a.txt", "D:/a.txt")              # OSError: Invalid cross-device link

os.rename asks the operating system to change one directory entry. That is instant, but it only works within a single filesystem. Crossing a drive boundary, or moving out of a Docker container’s mounted volume, fails.

shutil.move tries os.rename first, and falls back to copy-then-delete when that fails. So it always works, but a cross-drive move of a large file takes real time and needs enough free space for both copies at once.

Moving many files#

Python
from pathlib import Path
import shutil

source = Path("downloads")
target = Path("downloads/pdfs")
target.mkdir(exist_ok=True)

for pdf in source.glob("*.pdf"):
    shutil.move(str(pdf), str(target / pdf.name))
    print("moved", pdf.name)

Two habits worth adopting for anything that moves files in bulk. First, do a dry run:

Python
DRY_RUN = True

for pdf in source.glob("*.pdf"):
    destination = target / pdf.name
    print(("would move" if DRY_RUN else "moving"), pdf.name, "->", destination)
    if not DRY_RUN:
        shutil.move(str(pdf), str(destination))

Second, collect the list before you start moving. Modifying a folder while iterating over it can cause files to be skipped:

Python
files = list(source.glob("*.pdf"))     # materialise first
for pdf in files:
    shutil.move(str(pdf), str(target / pdf.name))

Handling errors#

Python
import shutil

try:
    shutil.move("report.pdf", "archive/report.pdf")
except FileNotFoundError:
    print("The source file does not exist.")
except PermissionError:
    print("No permission, or the file is open in another program.")
except shutil.Error as error:
    print("Move failed:", error)
except OSError as error:
    print("Filesystem error:", error)

On Windows, PermissionError most often means the file is open in another application. Windows locks open files in a way that Linux and macOS do not.

Copying instead of moving#

Python
shutil.copy("a.txt", "b.txt")       # contents and permission bits
shutil.copy2("a.txt", "b.txt")      # also timestamps and metadata
shutil.copyfile("a.txt", "b.txt")   # contents only, destination must be a file

shutil.copytree("src/", "dest/", dirs_exist_ok=True)
shutil.rmtree("old/")               # delete a folder and everything in it

Use copy2 by default — preserving the modification time is almost always what you want, and it is what shutil.move uses internally for the cross-drive case.

Questions people ask#

Does shutil.move work on folders?

Yes. Moving a folder moves everything inside it. Across filesystems it becomes a recursive copy followed by a recursive delete, which is much slower than it looks.

Can I move a file to a folder that does not exist?

Not directly — the trailing slash is not enough. Call Path(target).mkdir(parents=True, exist_ok=True) first.

Why is my cross-drive move so slow?

Because it is really a copy plus a delete. There is no way around that: the data physically has to be written to the other device.

Is there a way to move to the recycle bin instead of deleting?

Not in the standard library. The third-party send2trash package does it on Windows, macOS and Linux, and is worth using in anything that deletes files a person cares about.

Where to go next#

A Python file organiser you can finish tonightBuild it

Keep reading

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *