Python Rename File

Tested on Python 3.14.6 on Kali GNU/Linux Rolling (kali-rolling)
Applies to Linux, macOS, Windows
Python versions Path.rename() from 3.4, returned Path object from 3.8, Path.move() from 3.14
Privilege Normal user with write access to the directory holding the file
Scope Renaming and moving files with pathlib, os, and shutil: extension changes, overwrite policy, batch renames, atomic replacement, cross-filesystem moves, and directory renames. Does not cover copying files or reading their contents.
Related guides Python tutorial
Short on time?Beta Get the gist of this 18-minute read in seconds, then ask follow-up questions

Renaming a file in Python is a single call. The interesting part is what that call does when the new name is already taken, when the destination sits in a folder that does not exist yet, or when the file has to land on a different disk. pathlib.Path.rename() is what I reach for in new code: it changes the name, it can move the file into another directory on the same filesystem, and it hands back a Path pointing at the result.

Every runnable example below builds its own file inside a throwaway directory from tempfile.TemporaryDirectory(), so you can paste one straight into a shell and nothing in your own project gets renamed. A few snippets need either a real path on your machine or Python 3.14, and for those I show what my lab printed instead.


Python rename file quick reference

Goal Code
Rename in the same folder path.rename(path.with_name("new.txt"))
Change the extension path.rename(path.with_suffix(".tsv"))
Change the name, keep the extension path.rename(path.with_stem("new-name"))
Move into another folder path.rename(other_dir / path.name)
Overwrite the destination on any OS path.replace(target)
Rename a string path os.rename(old, new)
Replace a string path portably os.replace(old, new)
Swap a temp file into place os.replace(tmp, target) after flush() and os.fsync()
Move across filesystems shutil.move(source, target)
Move on Python 3.14+ path.move(target) or path.move_into(directory)
Batch rename by pattern for p in sorted(d.glob("*.log")): p.rename(p.with_name(f"backup_{p.name}"))
Rename a directory old_dir.rename(new_dir)

Rename a file in Python with pathlib

The shortest correct rename builds the new path from the old one, so the file keeps its folder and only the name changes.

python
from pathlib import Path
import tempfile

with tempfile.TemporaryDirectory() as d:
    source = Path(d) / "report.txt"
    source.write_text("quarterly numbers", encoding="utf-8")
    target = source.rename(source.with_name("report-final.txt"))
    print("old name still there?", source.exists())
    print("new name:", target.name)
    print("content kept:", target.read_text(encoding="utf-8"))
Output

That prints old name still there? False, new name: report-final.txt, and content kept: quarterly numbers. Two details matter here. with_name() swaps the filename and keeps the parent folder, which is what saves you from string surgery on slashes. And since Python 3.8, rename() returns a new Path for the destination, so you can chain reads and further calls onto the result instead of rebuilding the path by hand.

The source object itself does not change. It still points at report.txt, which no longer exists, so keep using the returned path after the rename.

Which renaming API to pick

Python gives you five ways to rename a file plus one new in 3.14, and they differ mainly in how they treat an existing destination.

Call Available since Existing destination Crosses filesystems Returns
Path.rename(target) 3.4 Replaced on Linux and macOS, FileExistsError on Windows No New Path (3.8+)
Path.replace(target) 3.4 Replaced on every platform No New Path (3.8+)
os.rename(src, dst) Always Same rules as Path.rename() No None
os.replace(src, dst) 3.3 Replaced on every platform No None
shutil.move(src, dst) Always Moved into the target when it is a directory, otherwise os.rename() rules Yes, copies then deletes Destination as str
Path.move(target) 3.14 Replaced when both are files, OSError for a non-empty directory Yes, copies then deletes New Path

One rule applies to all six: none of them create missing parent directories. That trips up most renames that also move the file.


Change only the file extension

When the name is fine and only the suffix is wrong, with_suffix() rewrites the extension and with_stem() rewrites everything before it.

python
from pathlib import Path
import tempfile

with tempfile.TemporaryDirectory() as d:
    source = Path(d) / "export.csv"
    source.write_text("id,name\n1,alice\n", encoding="utf-8")
    tsv = source.rename(source.with_suffix(".tsv"))
    print("new suffix:", tsv.name)
    dated = tsv.rename(tsv.with_stem("export-2026-08"))
    print("new stem:", dated.name)
Output

You get new suffix: export.tsv and then new stem: export-2026-08.tsv. Note that with_suffix() needs the leading dot, and that it replaces the last suffix only: on archive.tar.gz it turns .gz into whatever you pass and leaves .tar alone. with_stem() arrived in Python 3.9; on older versions use with_name(f"new-name{path.suffix}") instead. If you need to inspect the suffix before deciding, the get file extension in Python examples cover the multi-suffix cases.


Rename and move a file to another folder

A rename can change the directory part of the path too, as long as both paths stay on the same filesystem. What it will not do is create the destination folder for you.

python
from pathlib import Path
import tempfile

with tempfile.TemporaryDirectory() as d:
    root = Path(d)
    source = root / "notes.txt"
    source.write_text("meeting notes", encoding="utf-8")
    target = root / "archive" / "notes.txt"

    try:
        source.rename(target)
    except FileNotFoundError as exc:
        print("rename failed:", exc.strerror)

    target.parent.mkdir(parents=True, exist_ok=True)
    moved = source.rename(target)
    print("moved to:", moved.relative_to(root))
Output

The first attempt prints rename failed: No such file or directory, and after mkdir() the second one prints moved to: archive/notes.txt. The confusing part of that error is that both the source and the destination appear in the message even though the missing piece is the archive folder. Call mkdir(parents=True, exist_ok=True) before the rename whenever the layout may not exist yet, as the create nested directories guide shows in more detail, and read FileNotFoundError in Python when you need the full list of reasons that exception fires on a path operation.


Rename a file without overwriting an existing one

A rename onto a name that is already taken does not behave the same way on Linux and on Windows, which catches out anyone who develops on one and deploys on the other.

python
from pathlib import Path
import tempfile

with tempfile.TemporaryDirectory() as d:
    root = Path(d)
    source = root / "incoming.txt"
    target = root / "current.txt"
    source.write_text("new data", encoding="utf-8")
    target.write_text("old data", encoding="utf-8")

    source.rename(target)
    print("current.txt now holds:", target.read_text(encoding="utf-8"))
    print("incoming.txt survived?", source.exists())
Output

On my Linux lab it printed current.txt now holds: new data and incoming.txt survived? False. The old contents of current.txt are gone, and nothing warned me. The same code on Windows raises FileExistsError instead, because CPython calls the Windows move API without the replace-existing flag that os.replace() sets.

A test with if target.exists() before the rename covers the honest mistake of picking a name that is already used, and that is enough for a script you run by hand — see check if a file exists in Python for probe patterns. It is not enough for a service where two workers rename files into the same directory, because another process can create the destination in the gap between your check and your rename. Python has no rename flag that refuses to overwrite, but a hard link does the job on Linux and macOS: os.link() fails when the target exists, and the check and the creation happen in one system call, so there is no gap for another process to win.

python
import os
from pathlib import Path
import tempfile


def rename_no_clobber(source: Path, target: Path) -> Path:
    """Rename source to target, or raise FileExistsError if target is taken."""
    os.link(source, target)
    source.unlink()
    return target


with tempfile.TemporaryDirectory() as d:
    root = Path(d)
    (root / "keep.txt").write_text("keep me", encoding="utf-8")
    (root / "incoming.txt").write_text("fresh", encoding="utf-8")

    try:
        rename_no_clobber(root / "incoming.txt", root / "keep.txt")
    except FileExistsError as exc:
        print("refused:", exc.strerror)

    renamed = rename_no_clobber(root / "incoming.txt", root / "final.txt")
    print("renamed to:", renamed.name)
    print("keep.txt untouched:", (root / "keep.txt").read_text(encoding="utf-8"))
Output

The output reads refused: File exists, then renamed to: final.txt and keep.txt untouched: keep me. The link-then-unlink pair is not a single atomic operation the way a rename is, so a crash between the two calls leaves both names pointing at the same data. That is a far friendlier failure than a destroyed file, and both names are on the same filesystem by definition here, which is exactly where hard links work.


Overwrite the destination on purpose with replace()

When replacing the destination is the whole point, say so in the code. Path.replace() overwrites on every platform, which removes the Unix versus Windows difference from the previous section.

python
from pathlib import Path
import tempfile

with tempfile.TemporaryDirectory() as d:
    root = Path(d)
    live = root / "config.json"
    staged = root / "config.json.tmp"
    live.write_text('{"debug": false}', encoding="utf-8")
    staged.write_text('{"debug": true}', encoding="utf-8")

    staged.replace(live)
    print("config.json:", live.read_text(encoding="utf-8"))
    print("staged file left behind?", staged.exists())
Output

It prints config.json: {"debug": true} and staged file left behind? False. Nothing was copied into place. The staged file became the live file in one operation, so there is no moment where config.json is half written. That property is what makes replace() the last step of the atomic write pattern shown later on this page.


Rename a file with os.rename() and os.replace()

Plenty of code predates pathlib or works with paths that arrive as strings from a config file or a web request. The os functions take those directly.

python
import os
import tempfile

with tempfile.TemporaryDirectory() as d:
    old = os.path.join(d, "app.log")
    new = os.path.join(d, "app.log.1")
    with open(old, "w", encoding="utf-8") as f:
        f.write("started\n")

    os.rename(old, new)
    print("old path gone:", not os.path.exists(old))
    print("new name:", os.path.basename(new), os.path.exists(new))
Output

This prints old path gone: True and new name: app.log.1 True. Build those string paths with os.path.join rather than gluing separators together, otherwise the code breaks the first time it runs on a platform with a different separator.

os.rename() returns None, so there is no new path to keep. The overwrite rules match Path.rename(), which means os.replace() is the portable choice when the destination may exist:

  • os.rename() replaces an existing file on Linux and macOS, and raises FileExistsError on Windows
  • os.replace() replaces an existing file on all three, and is what you want in cross-platform code
  • both raise IsADirectoryError or NotADirectoryError when you cross a file with a directory
  • both are atomic on POSIX, so a reader sees either the old name or the new one, never a partial state

Path.rename() and Path.replace() call straight into os.rename() and os.replace(), so there is no behaviour difference to learn beyond the argument types and the return value.


Replace a file safely while it is being read

Writing straight to a live file has a window where the file is half written, and a reader that opens it during that window gets a truncated document. Write the bytes to a temp file first — the write to file in Python guide covers modes and flushing — then replace the original in one step so the swap is a single filesystem operation.

python
import os
import tempfile
from pathlib import Path


def write_atomic(target: Path, data: str) -> None:
    """Write data to target so readers never see a half-written file."""
    fd, tmp_name = tempfile.mkstemp(dir=target.parent, prefix=target.name, suffix=".tmp")
    tmp = Path(tmp_name)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(data)
            f.flush()
            os.fsync(f.fileno())
        os.replace(tmp, target)
    except BaseException:
        tmp.unlink(missing_ok=True)
        raise


with tempfile.TemporaryDirectory() as d:
    settings = Path(d) / "settings.json"
    write_atomic(settings, '{"workers": 2}')
    write_atomic(settings, '{"workers": 8}')
    print(settings.read_text(encoding="utf-8"))
    print("leftover temp files:", sorted(p.name for p in settings.parent.iterdir()))
Output

The final read prints {"workers": 8} and the directory listing prints ['settings.json'], which is the part worth checking: the second write replaced the first with no debris left behind. Three choices in that function are deliberate:

  • The temp file is created with dir=target.parent so it lands on the same filesystem, because os.replace() cannot cross one
  • flush() and os.fsync() push the bytes to disk before the swap, so a power loss cannot leave an empty file under the real name
  • The except BaseException block removes the temp file with unlink(missing_ok=True) on any failure, including KeyboardInterrupt — the same cleanup pattern as delete a file in Python — and then re-raises so the caller still sees the error

Rename or move a file with Path.move() on Python 3.14

Python 3.14 added Path.move() and Path.move_into(), which finally cover the case rename() never could: a destination on another filesystem. Under the hood move() uses os.replace() when both paths share a filesystem and falls back to copy-then-delete when they do not.

python
from pathlib import Path
import tempfile

with tempfile.TemporaryDirectory() as d:
    root = Path(d)
    source = root / "readme.txt"
    source.write_text("hello", encoding="utf-8")

    renamed = source.move(root / "readme.md")
    print("renamed:", renamed.name)

    archive = root / "archive"
    archive.mkdir()
    moved = renamed.move_into(archive)
    print("moved into:", moved.relative_to(root))

On my Python 3.14.6 lab that printed renamed: readme.md and moved into: archive/readme.md. The in-browser Run button is switched off for this snippet because the sandbox runs Python 3.13, where these two methods do not exist yet.

move_into() is the ergonomic half of the pair: pass a directory and the filename comes along unchanged, instead of writing source.move(archive / source.name). Both methods still expect the destination directory to exist. I tried move_into() against a missing folder and got FileNotFoundError, the same as with rename().

Keep shutil.move() in code that must also run on 3.13 or older, and use Path.move() once your minimum supported version is 3.14.


Rename multiple files in a folder

Batch renames are where the small pathlib helpers pay off, because the loop body stays readable. Collect the files with glob(), then rename each one relative to its own parent. A Python for loop over sorted() keeps the order predictable, which matters as soon as the new names contain a counter.

python
from pathlib import Path
import tempfile

with tempfile.TemporaryDirectory() as d:
    root = Path(d)
    for name in ("alpha.log", "beta.log", "notes.txt"):
        (root / name).write_text(name, encoding="utf-8")

    for path in sorted(root.glob("*.log")):
        path.rename(path.with_name(f"backup_{path.name}"))

    print(sorted(p.name for p in root.iterdir()))
Output

The listing prints ['backup_alpha.log', 'backup_beta.log', 'notes.txt'], so the two log files picked up the prefix and notes.txt was never a candidate. Use rglob("*.log") instead of glob() when you want the same rename applied through subdirectories.

Numbering a batch works the same way, with enumerate() supplying the counter and an f-string zero-padding it.

python
from pathlib import Path
import tempfile

with tempfile.TemporaryDirectory() as d:
    root = Path(d)
    for name in ("DSC_4821.jpg", "DSC_4822.jpg", "IMG20260714.jpg"):
        (root / name).write_text("binary-ish", encoding="utf-8")

    for index, path in enumerate(sorted(root.glob("*.jpg")), start=1):
        path.rename(path.with_name(f"goa-2026-{index:03d}{path.suffix}"))

    print(sorted(p.name for p in root.iterdir()))
Output

That gives ['goa-2026-001.jpg', 'goa-2026-002.jpg', 'goa-2026-003.jpg']. Reusing path.suffix keeps the original extension, which is safer than hard-coding .jpg when the folder holds a mix of .jpg and .jpeg. One warning about counters: if a target name already exists in the same folder, the rename silently overwrites it on Linux, so build the whole list of new names first and check for duplicates before you touch anything.

Keep renamed files in their own directory

A relative target is resolved against the process working directory, not against the folder the source file lives in, so a bare filename quietly relocates the file instead of renaming it in place. This is the batch mistake I see most often.

python
for path in sorted(reports.glob("*.txt")):
    path.rename("q1-final.txt")   # lands in the current working directory

I ran that shape against a reports/ folder while the working directory was somewhere else, and the file landed in the working directory: the listing there showed q1-final.txt while reports/ came back empty. Nothing raised an error, which is why it usually goes unnoticed until a user asks where their files went.

Always give the rename a target anchored to the source. Both of these forms are safe:

python
path.rename(path.with_name("q1-final.txt"))   # same folder, new name
path.rename(path.parent / "q1-final.txt")     # same thing, spelled out

Move a file across filesystems

A rename is a metadata change inside one filesystem, so the kernel refuses when the destination is on a different mount. The failure is an OSError carrying errno.EXDEV, and the fix is to copy the data and delete the source, which is exactly what shutil.move() does.

python
import errno
import shutil
from pathlib import Path

source = Path("/dev/shm/report.csv")
source.write_text("id,value\n1,42\n", encoding="utf-8")
target = Path.home() / "report.csv"

try:
    moved = source.rename(target)
except OSError as exc:
    if exc.errno != errno.EXDEV:
        raise
    print("rename refused:", exc.strerror)
    moved = Path(shutil.move(source, target))

print("moved to:", moved)
print("still on source path?", source.exists())
moved.unlink()

On my lab /dev/shm is tmpfs and /root is ext4, so the rename failed and the fallback took over. The run printed rename refused: Invalid cross-device link, then moved to: /root/report.csv and still on source path? False. This snippet writes to real paths on your machine rather than a temp directory, so the Run button is off; change both paths to two of your own mounts if you want to reproduce it.

Two things to remember about the fallback. shutil.move() returns the destination as a string, hence the Path() wrapper, and it moves the source inside the destination when the destination is an existing directory, so shutil.move("a.txt", "inbox") produces inbox/a.txt. Also, a cross-filesystem move is not atomic: it is a copy followed by a delete, and an interrupted run can leave a partial file at the destination.


Rename a directory in Python

Path.rename() does not care whether the path is a file or a directory. Renaming a folder moves everything under it, because only the directory entry changes.

python
from pathlib import Path
import tempfile

with tempfile.TemporaryDirectory() as d:
    root = Path(d)
    old = root / "project-v1"
    (old / "src").mkdir(parents=True)
    (old / "src" / "main.py").write_text("print('hi')\n", encoding="utf-8")

    new = old.rename(root / "project-v2")
    print("old directory gone:", not old.exists())
    print(sorted(str(p.relative_to(root)) for p in new.rglob("*")))
Output

The output is old directory gone: True followed by ['project-v2/src', 'project-v2/src/main.py']. The nested file came along untouched and nothing was copied, which is why renaming a directory is instant even when it holds gigabytes. The one restriction is that a directory target must be empty: renaming onto a directory that still holds files raises OSError with Directory not empty.


Errors you will hit when renaming files

Wrap renames in try except and catch the specific exception rather than a bare except, since each of these needs a different fix. The error text below is what Python 3.14.6 printed on my lab, apart from the two Windows-only cases.

Error What happened Fix
FileNotFoundError: No such file or directory The source is missing, or the destination folder does not exist Confirm the source path, then mkdir(parents=True, exist_ok=True) on target.parent
FileExistsError: File exists On Windows the destination name is taken; on Linux from os.link() Use Path.replace() to overwrite deliberately, or pick a free name
IsADirectoryError: Is a directory The source is a file and the target is an existing directory Rename to target / source.name instead of to the directory itself
NotADirectoryError: Not a directory The source is a directory and the target is an existing file Delete or rename the blocking file first
OSError: Directory not empty The target directory exists and holds files Merge the contents yourself, or remove the target tree first
OSError: Invalid cross-device link Source and destination are on different filesystems Fall back to shutil.move(), or Path.move() on 3.14+
PermissionError: Permission denied No write permission on either directory, or the file is open on Windows Fix ownership and permissions; close handles before renaming on Windows

A rename needs write and execute permission on the directories involved, not on the file itself. That is why a read-only file renames fine while a rename inside a read-only folder fails.


Common mistakes when renaming files in Python

  • Passing a bare filename as the target, which resolves against the working directory instead of the source folder
  • Expecting rename() to create the destination folder, when only mkdir(parents=True) does that
  • Assuming a rename fails when the destination exists, which is true on Windows and false on Linux and macOS
  • Reusing the original Path object after the rename instead of the Path that rename() returned
  • Renaming across a mount point or a Windows drive letter without a shutil.move() fallback
  • Renaming files while iterating the same directory with iterdir(), which can visit a file twice or skip one; build the list first with sorted(glob(...))
  • Calling with_suffix(".gz") on archive.tar.gz and expecting both suffixes to change
  • Trusting exists() immediately before a rename in concurrent code, where another process can win the gap

Summary

For new code, reach for Path.rename() and build the target with with_name(), with_suffix(), or with_stem() so the folder part of the path is never in question. Keep the Path it returns, since the object you called it on now points at a name that is gone. When the destination might already exist, decide the policy explicitly: Path.replace() and os.replace() overwrite the same way on every platform, while Path.rename() and os.rename() overwrite on Linux and macOS but raise FileExistsError on Windows. That difference is the single most common reason a rename behaves one way on a laptop and another way in production.

Two limits show up as soon as a rename also moves the file. Nothing in this API creates parent directories, so call mkdir(parents=True, exist_ok=True) first, and no rename can cross a filesystem, so catch OSError with errno.EXDEV and hand the job to shutil.move(). Python 3.14 folds that fallback into Path.move() and Path.move_into(), which is genuinely nicer, though shutil.move() remains the portable answer while you support 3.13 and older.

For batch work, list the files first with sorted(glob(...)), anchor every new name to path.parent or with_name(), and check your generated names for collisions before the loop starts renaming. If the file is a config or data file that other processes read, use the temp-file-plus-os.replace() pattern rather than writing the live path directly, and readers will only ever see a complete version of the file.


References


Frequently Asked Questions

How do I rename a file in Python?

Build the new path from the old one and pass it to Path.rename(), for example source.rename(source.with_name("new.txt")), which keeps the file in the same folder and returns a Path pointing at the new name.

Does Path.rename() overwrite an existing file?

On Linux and macOS it replaces the destination silently, while on Windows it raises FileExistsError, so use Path.replace() when you want the same overwrite behaviour everywhere or check the target yourself when you want the rename to fail.

What is the difference between os.rename() and os.replace()?

They are identical on Linux and macOS, but os.replace() also overwrites an existing destination on Windows instead of raising FileExistsError, which makes it the portable choice for replacing a file.

Why do my batch renamed files end up in the wrong folder?

A relative target is resolved against the current working directory rather than the folder of the source file, so pass path.with_name("new.txt") or path.parent / "new.txt" instead of a bare filename.

How do I move a file to another disk or partition in Python?

Renaming across filesystems fails with OSError and errno EXDEV, so fall back to shutil.move(), which copies the data and deletes the source, or use Path.move() on Python 3.14 and newer.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)