Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 9 min read Last reviewed Jul 2026

Python Error Guide: 'PermissionError: [Errno 13] Permission denied' — Fix File Access

Quick answer

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
Free toolkit

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) raises PermissionError naming a specific path.
  • The script works when run with sudo or as root but fails as the service/automation user.
  • Writing to a system directory (/var/log, /etc, /opt) fails while writing to /tmp or 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 root from a previous sudo run) 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 yields EACCES.
  • 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 mkdir the directories your service uses; create them as (or chown them 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_SERVICE instead 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.

Free download · 368-page PDF

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?

Free download · 368-page PDF

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.