The metadata that says no
Lesson 7-3 showed that every file carries metadata. One piece of it, permissions, is the gatekeeper. It tells the OS who may do what with the file, and the OS checks it on every open.
You will collide with permissions in your first weeks of real work: a script failing with PermissionError, a downloaded tool that refuses to run until chmod +x, an SSH key that ssh rejects because others can read it. This lesson is so those moments take seconds instead of an afternoon.
On macOS and Linux, each file stores three permission flags for three audiences.
| Flag | Allows |
|---|---|
r | reading the bytes |
w | writing or changing them |
x | executing the file as a program |
The audiences are the file's owner, the file's group, which is a named set of users, and others, meaning everyone else.
That is 9 yes-or-no switches per file, and the kernel consults them before your code sees a single byte. A denied check surfaces in Python as a PermissionError, because the request crossed into the OS and the OS said no.
A read-only file rejecting a write
Mode 0o400 allows the owner to read and nothing else at all.
import os import stat with open("secret.txt", "w") as f: f.write("api-key-123\n") os.chmod("secret.txt", 0o400) print(stat.filemode(os.stat("secret.txt").st_mode)) try: open("secret.txt", "w") print("write allowed") except PermissionError: print("write blocked: the OS refused before one byte moved") with open("secret.txt") as f: print("read still works:", f.read(), end="")
Output
-r--------
write blocked: the OS refused before one byte moved
read still works: api-key-123os.chmod sets the permission bits and stat.filemode renders the 9 switches the way ls -l does. Read the string in groups of three after the leading dash: r-- for the owner, then --- for the group, then --- for others.
0o400 is exactly the mode ssh demands for private key files. If the group or other bits are set, ssh refuses the key outright, which is the single most common reason a freshly copied key does not work.
Reading and writing the numbers
Permissions are usually written as three octal digits, base 8, one per audience, because each digit packs the three flags as a sum where r is 4, w is 2, and x is 1.
| Digit | Flags |
|---|---|
| 7 | rwx |
| 6 | rw- |
| 5 | r-x |
| 4 | r-- |
| 0 | --- |
Python spells octal with the 0o prefix, the same idea as 0b and 0x from lesson 2-1. The combinations you will actually type:
| Mode | Meaning | Typical use |
|---|---|---|
755 | owner rwx, everyone else r-x | programs and scripts |
644 | owner rw-, everyone else r-- | ordinary data files |
600 | owner rw-, no one else anything | private config, credentials |
400 | owner read-only | SSH private keys |
In the terminal the tool is chmod, short for change mode, as in chmod 755 deploy.sh, or the shortcut chmod +x tool.sh to switch on execute for everyone. The execute bit is why a freshly written script answers Permission denied, since the shell asked the OS to execute a file whose x switches are off.
One honest caveat: the root user, the administrator account the OS itself uses, bypasses these checks. Permissions protect against accidents and other users, not against whoever owns the machine.
Making a script executable
Mode 755 gives the owner full access and everyone else read plus execute.
import os import stat with open("tool.sh", "w") as f: f.write("echo hi\n") os.chmod("tool.sh", 0o755) print(stat.filemode(os.stat("tool.sh").st_mode))
Output
-rwxr-xr-x
The octal literal must be written as 0o755, since a plain 755 is the decimal number seven hundred fifty-five and a completely different bit pattern. This is exactly what chmod +x does when it makes a script runnable.
Reading the output string confirms the intent: rwx for the owner, then r-x twice for group and others. Everyone can run it and only the owner can change it, which is the normal arrangement for an installed program.
Permission denied on a script you can read
A cloned project whose ./deploy.sh answers "Permission denied" while cat reads it fine has its execute bit off, and chmod +x deploy.sh fixes it.
Read and execute are separate switches. cat needs r, running the file as a program needs x, and only the x bit is missing.
| Action | Bit required |
|---|---|
cat deploy.sh | r |
./deploy.sh | x |
| editing it | w |
That r and x are independent is a feature rather than an inconvenience. Config files stay readable but not runnable by design, and a data file that could be executed would be a security problem rather than a convenience.