| 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 |
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.
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"))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.
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)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.
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))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.
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())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.
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"))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.
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())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.
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))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 raisesFileExistsErroron Windowsos.replace()replaces an existing file on all three, and is what you want in cross-platform code- both raise
IsADirectoryErrororNotADirectoryErrorwhen 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.
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()))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.parentso it lands on the same filesystem, becauseos.replace()cannot cross one flush()andos.fsync()push the bytes to disk before the swap, so a power loss cannot leave an empty file under the real name- The
except BaseExceptionblock removes the temp file withunlink(missing_ok=True)on any failure, includingKeyboardInterrupt— 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.
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.
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()))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.
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()))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.
for path in sorted(reports.glob("*.txt")):
path.rename("q1-final.txt") # lands in the current working directoryI 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:
path.rename(path.with_name("q1-final.txt")) # same folder, new name
path.rename(path.parent / "q1-final.txt") # same thing, spelled outMove 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.
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.
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("*")))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 onlymkdir(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
Pathobject after the rename instead of thePaththatrename()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 withsorted(glob(...)) - Calling
with_suffix(".gz")onarchive.tar.gzand 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
- pathlib.Path.rename and pathlib.Path.replace
- pathlib.Path.move and
Path.move_into(Python 3.14+) - os.rename and os.replace
- shutil.move
- What's new in Python 3.14: pathlib
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.

