shutil.rmtree PermissionError: [Errno 13] (tested)
shutil.rmtree raising PermissionError: [Errno 13] Permission denied on Linux or macOS is usually about a directory you can't write to, not a read-only file. We measured it on Python 3.13.5 along with an onexc handler that fixes it.
1. On Linux and macOS, deleting a file needs write permission on its directory
import os, shutil
os.makedirs("t/locked")
open("t/locked/f.txt", "w").close()
os.chmod("t/locked", 0o555) # read+execute, no write: can't delete entries
try:
shutil.rmtree("t")
except PermissionError as e:
print("rmtree:", e)
print("f.txt still there:", os.path.exists("t/locked/f.txt"))
shutil.rmtree("t", ignore_errors=True)
print("after ignore_errors=True, t exists:", os.path.exists("t"))
Output:
rmtree: [Errno 13] Permission denied: 't/locked'
f.txt still there: True
after ignore_errors=True, t exists: True
Measured on Python 3.13.5 (Linux): rmtree fails with [Errno 13] Permission denied, and the message names the directory t/locked, not the file. That's the clue: removing an entry is a write to its parent directory. ignore_errors=True doesn't fix anything; it silently leaves the whole tree in place.
2. Fix: an onexc handler that adds write permission and retries
Run right after the block above, on the same tree:
import os, shutil, stat
def make_writable_and_retry(func, path, exc):
if not isinstance(exc, PermissionError):
raise exc
os.chmod(os.path.dirname(path), stat.S_IRWXU) # the parent dir needs write permission
func(path)
shutil.rmtree("t", onexc=make_writable_and_retry)
print("t exists:", os.path.exists("t"))
Output:
t exists: False
onexc is new in Python 3.12 and receives the exception itself; older code uses onerror, which gets sys.exc_info() instead and is deprecated since 3.12. Re-raising anything that isn't a PermissionError keeps real problems visible. Only use this on trees you own: it changes permissions to delete things someone protected.
3. A read-only file is not the problem on Linux
import os, shutil
os.makedirs("t3")
open("t3/ro.txt", "w").close()
os.chmod("t3/ro.txt", 0o444) # read-only file, writable directory
shutil.rmtree("t3")
print("t3 exists:", os.path.exists("t3"))
Output:
t3 exists: False
Measured: a 0o444 file in a writable directory is deleted without any error on Linux. On Windows a read-only file attribute does block deletion and gives a PermissionError (WinError 5 or WinError 32 if another process has the file open); we did not test Windows here. The usual Windows handler clears the attribute with os.chmod(path, stat.S_IWRITE) then retries func(path); WinError 32 means close the other program first.
4. Checklist
- Read the path in the error: it's the directory you can't write to.
- Files created by Docker or
sudoare often owned by root;chmodwon't help without ownership. Check withls -ld. - Don't use
ignore_errors=Trueas a fix; checkos.path.exists()afterwards if you do.
Frequently asked questions
Why does shutil.rmtree give PermissionError: [Errno 13]?
On Linux and macOS, deleting an entry needs write permission on the directory containing it. We measured rmtree failing on a 0o555 directory, with the error naming that directory.
How do I force shutil.rmtree to delete read-only files?
Pass an onexc handler (Python 3.12+) that fixes permissions and calls func(path) again. We measured chmod-ing the parent directory and retrying removing the tree.
Does shutil.rmtree ignore_errors=True delete everything?
No. It skips what it can't delete without telling you. We measured the whole tree still existing afterwards.
Can rmtree delete a read-only file on Linux?
Yes, if the directory is writable. We measured a 0o444 file removed with no error. Read-only files block deletion on Windows, which we did not test.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5 on Linux: rmtree on a tree containing a read+execute-only directory, ignore_errors=True, an onexc handler that restores write permission and retries, and a read-only file in a writable directory. Every block was run verbatim and its output pasted from that run. Windows behaviour was not tested. PlainHeaders is our extension; everything else here is built-in Python.