Python Error Guide: 'PermissionError: [Errno 13] Permission denied' — Fix File Access
Fix 'PermissionError: [Errno 13] Permission denied' in Python: correct file ownership and modes, handle privileged ports, and run automation as the right user.
- #python
- #automation
- #troubleshooting
- #errors
Stuck on this Bash & Python Automation error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
PermissionError: [Errno 13] Permission denied is Python surfacing the operating system’s EACCES error: the process tried an operation — usually opening, writing, or executing a path — that the running user is not permitted to perform. It most often appears from open():
Traceback (most recent call last):
File "backup.py", line 18, in <module>
with open("/var/log/backup.log", "a") as fh:
PermissionError: [Errno 13] Permission denied: '/var/log/backup.log'
The path at the end of the message is the exact resource that was denied, and [Errno 13] confirms it is a permissions problem (as opposed to [Errno 2], a missing file, or [Errno 21], a directory).
Symptoms
open(...)for writing (or reading) raisesPermissionErrornaming a specific path.- The script works when run with
sudoor asrootbut fails as the service/automation user. - Writing to a system directory (
/var/log,/etc,/opt) fails while writing to/tmpor the home directory succeeds. - Binding a socket to a port below 1024 raises
PermissionError. - A file that exists and is readable by you cannot be opened by the running process (ownership or ACL mismatch).
- Writing works on one host and fails on another mounted read-only or with different ownership.
Common Root Causes
- Insufficient file/directory permissions — the process user lacks read or write on the target path or a parent directory.
- Wrong ownership — the file is owned by another user (often
rootfrom a previoussudorun) and the mode doesn’t grant the running user access. - Trying to open a directory as a file — on some paths this yields
IsADirectoryError(Errno 21), but attempting to write into a directory you can’t modify yieldsEACCES. - Missing directory traverse bit — a parent directory lacks the execute (
x) bit, so the process can’t reach the file inside it. - Read-only filesystem or mount — the target is on a read-only mount (though a pure read-only FS raises
Errno 30, permission-style denials are common on network mounts and containers). - Privileged resource — binding to a port < 1024, or writing to a root-owned socket/PID path, without the needed capability.
- SELinux / AppArmor — mandatory access control denies the operation even when classic Unix bits allow it.
- Running as the wrong user under systemd/cron — the unit’s
User=(or crontab owner) lacks rights the developer’s login shell has.
Diagnostic Workflow
First, capture the full traceback and the exact path — never swallow the exception while debugging:
python backup.py
Inspect the target and its parents from the shell, as the same user the process runs as:
ls -ld /var/log /var/log/backup.log # mode + owner of file and its directory
id # the current user's uid/gid/groups
namei -l /var/log/backup.log # permissions of every path component
Confirm from inside Python what the process can actually do, and who it is:
import os
path = "/var/log/backup.log"
print("euid:", os.geteuid(), "user:", os.getlogin() if os.isatty(0) else "n/a")
print("dir writable:", os.access(os.path.dirname(path), os.W_OK))
print("file readable:", os.access(path, os.R_OK), "writable:", os.access(path, os.W_OK))
If classic bits look fine, check ACLs and SELinux:
getfacl /var/log/backup.log
sudo ausearch -m avc -ts recent 2>/dev/null | tail # SELinux denials
For the wrong-user case under systemd, verify which user the service runs as:
systemctl show my-job.service -p User -p Group
Example Root Cause Analysis
A data-export job ran fine when developed interactively but failed every night under systemd:
Traceback (most recent call last):
File "/opt/export/run.py", line 24, in write_report
with open("/opt/export/out/report.csv", "w") as fh:
PermissionError: [Errno 13] Permission denied: '/opt/export/out/report.csv'
The developer had created /opt/export/out/ earlier with sudo mkdir, so it was owned by root:
$ ls -ld /opt/export/out
drwxr-xr-x 2 root root 4096 Jul 9 01:00 /opt/export/out
$ systemctl show export.service -p User
User=exportsvc
The service ran as exportsvc, but the output directory was root-owned and mode 755 — group and others had no write bit. exportsvc could traverse and read the directory but not create a file in it, so open(..., "w") was denied. The fix granted the service user ownership of just what it needs, at the minimum privilege:
sudo chown -R exportsvc:exportsvc /opt/export/out
sudo chmod 750 /opt/export/out
The code was also hardened to fail with a clear, actionable message instead of a raw traceback:
import os, sys
out_dir = "/opt/export/out"
if not os.access(out_dir, os.W_OK):
sys.exit(f"FATAL: {out_dir} is not writable by uid {os.geteuid()}; "
f"check ownership/mode (need write for the service user)")
with open(os.path.join(out_dir, "report.csv"), "w") as fh:
fh.write(data)
Prevention Best Practices
- Run automation as a dedicated user and give that user explicit ownership of the directories it must write — don’t rely on
sudo, which masks the real permissions. - Never
sudo mkdirthe directories your service uses; create them as (orchownthem to) the service user so nightly runs match your interactive setup. - Preflight writability — check
os.access(dir, os.W_OK)(or attempt-and-catch) early and exit with a clear message rather than deep inside the job. - Avoid
chmod 777— it hides the ownership problem and is a security risk; grant the minimum mode (e.g.750) to the right user/group. - Write to user-owned or configurable paths — take the output directory from config/env so the same code works across users and hosts.
- Drop privileges intentionally — if you must start as root (e.g. to bind port 80), bind first, then drop to an unprivileged user, or use
CAP_NET_BIND_SERVICEinstead of running the whole job as root. - Account for SELinux/AppArmor — label paths correctly (
chcon/policy) rather than disabling enforcement.
Quick Command Reference
ls -ld PATH DIR # mode and owner of file and parent
namei -l PATH # permissions of every path component
id # current user/groups
getfacl PATH # inspect ACLs
systemctl show UNIT -p User # which user the service runs as
sudo chown USER:GROUP PATH # fix ownership (least privilege)
python -c "import os;print(os.access('DIR', os.W_OK))" # writable?
sudo ausearch -m avc -ts recent # SELinux denials
Conclusion
PermissionError: [Errno 13] is the OS refusing an operation because the running user lacks the right on the exact path in the message. The most common real-world cause is a mismatch between the user you developed as and the user the job actually runs as under cron or systemd — often a root-owned directory a service user can’t write. Diagnose by checking ls -ld and id as the real runtime user, fix by granting the service user least-privilege ownership (never chmod 777), and harden the code to preflight writability and fail with an actionable message.
Fixed it? Get 500 Bash & Python Automation & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.