PermissionError: [Errno 13] Permission denied (tested)
PermissionError: [Errno 13] Permission denied means the OS refused what your script asked for. We reproduced each common cause on Python 3.13.5 on Linux and tested a fix for each that doesn't involve sudo or chmod 777.
1. What raises it
import os, socket, stat
os.makedirs("reports", exist_ok=True)
with open("locked.txt", "w") as f:
f.write("x")
os.chmod("locked.txt", stat.S_IRUSR) # read-only for the owner
tests = {
"open a directory": lambda: open("reports"),
"write read-only file": lambda: open("locked.txt", "w"),
"write to /etc": lambda: open("/etc/myapp.conf", "w"),
"bind port 80": lambda: socket.socket().bind(("127.0.0.1", 80)),
}
for name, f in tests.items():
try:
f()
print(f"{name}: ok")
except OSError as e:
print(f"{name}: {type(e).__name__}: {e}")
os.chmod("locked.txt", stat.S_IRUSR | stat.S_IWUSR)
open a directory: IsADirectoryError: [Errno 21] Is a directory: 'reports'
write read-only file: PermissionError: [Errno 13] Permission denied: 'locked.txt'
write to /etc: PermissionError: [Errno 13] Permission denied: '/etc/myapp.conf'
bind port 80: PermissionError: [Errno 13] Permission denied
Measured (Linux, as a normal user): writing to a file whose mode is read-only, creating a file in a directory you don't own (/etc), and binding a port below 1024 all raised PermissionError: [Errno 13] Permission denied. The port error has no filename in it, which is how you tell it apart. Opening a directory as a file raised IsADirectoryError: [Errno 21] here; on Windows the same mistake is widely reported as PermissionError: [Errno 13] instead (we didn't test Windows), so on Windows check first that the path isn't a folder.
2. Fixes
import os, stat
os.makedirs("reports", exist_ok=True)
path = os.path.join("reports", "summary.txt") # a file inside the folder
with open(path, "w") as f:
f.write("ok")
print(open(path).read())
print(os.access("/etc", os.W_OK), os.access(".", os.W_OK))
mode = stat.filemode(os.stat("/etc").st_mode)
print("/etc", mode, "owner uid", os.stat("/etc").st_uid, "me", os.getuid())
ok
False True
/etc drwxr-xr-x owner uid 0 me 1000
- Path is a folder: join a filename onto it (
os.path.join("reports", "summary.txt")), as above. - Directory you don't own:
os.access(path, os.W_OK)told us/etcisn't writable (owner uid 0, modedrwxr-xr-x) while the current directory is. Write somewhere you own (your project, the user's home,tempfile) rather than running the script withsudo. - Read-only file: check its mode with
ls -lorstat.filemode(os.stat(p).st_mode)and fix ownership or permissions on purpose; don'tchmod 777. - Port below 1024: use a port of 1024 or above (8000, 8080) in development, and put a reverse proxy in front for 80/443.
- Windows, file in use: a file open in Excel or another program gives
PermissionError: [WinError 32]or Errno 13 there; close it and retry (not tested here).
Frequently asked questions
What does PermissionError: [Errno 13] Permission denied mean?
The operating system refused the operation for your user. We measured it on Linux from writing a read-only file, creating a file in /etc, and binding port 80 as a normal user.
Why do I get Permission denied when opening a folder in Python?
You passed a directory to open(). On Linux we measured IsADirectoryError [Errno 21]; on Windows it's reported as PermissionError [Errno 13]. Join a filename onto the folder path.
Should I run my Python script with sudo to fix Errno 13?
Usually not. Write to a location you own instead; we measured os.access() showing /etc not writable and the current directory writable for a normal user.
Why can't Python bind to port 80?
Ports below 1024 need extra privileges on Linux. We measured PermissionError [Errno 13] with no filename; use 1024 or above, or a reverse proxy.
How do I check permissions from Python?
os.access(path, os.W_OK) for writability and stat.filemode(os.stat(path).st_mode) for the mode string; we measured both.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5 on Linux as a non-root user: writing a chmod-read-only file, creating a file in /etc, binding 127.0.0.1:80, opening a directory with open(), and the os.access/stat checks. Every code block was run verbatim and its output pasted from that run. Windows behavior is noted from common reports, not tested. PlainHeaders is our extension; everything else here is built-in Python.