os.chmod
chmod sets the complete permission mode; umask decides which bits new files do NOT get. On Windows chmod only toggles the read-only flag (the write bits) and ignores everything else. chown, fchown and lchown are Unix only; lchmod exists on Windows (3.13+) and macOS/BSD but not on Linux.
Common call
os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR)
Returns
None (umask: the previous mask)
Replaces
subprocess.run(["chmod", "+x", path])
Watch out
Octal literals: 0o644, not 644
os.chmod(pathpath — The file. chmod and chown also accept an open file descriptor (on Windows since 3.13 for chmod).type: str | bytes | PathLike | int · required, modemode — chmod/fchmod/lchmod: permission bits, an octal literal (0o644) or stat.S_* flags OR-ed together.type: int · required, *, dir_fddir_fd — Resolve a relative path against this open directory (Unix).type: int · default: None=None, follow_symlinksfollow_symlinks — False acts on a symlink itself (lchmod / lchown). The docs note the default is False on Windows.type: bool · default: True=True)
→ None | int
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| path | str | bytes | PathLike | int | yes | The file. chmod and chown also accept an open file descriptor (on Windows since 3.13 for chmod). |
| mode | int | yes | chmod/fchmod/lchmod: permission bits, an octal literal (0o644) or stat.S_* flags OR-ed together. |
| uid | int | yes | chown family: numeric user id; -1 leaves it unchanged. |
| gid | int | yes | chown family: numeric group id; -1 leaves it unchanged. |
| mask | int | yes | umask: bits to remove from the mode of newly created files and directories. |
| dir_fd | int | no (None) | Resolve a relative path against this open directory (Unix). |
| follow_symlinks | bool | no (True) | False acts on a symlink itself (lchmod / lchown). The docs note the default is False on Windows. |
Return value
None | int — chmod/chown and their variants return None; umask returns the previous mask as an int.
Common patterns
Make a script executable (Unix)
Add the execute bit for owner, group and others to the current mode.
import os import stat mode = os.stat('deploy.sh').st_mode os.chmod('deploy.sh', mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
Clear read-only before deleting (works on Windows)
A read-only file cannot be deleted on Windows; give the write bit back first.
import os import stat os.chmod(path, stat.S_IWRITE) os.remove(path)
Create files with a private mask
Set the umask around the code that creates files, and always restore it - it is process-wide.
import os old = os.umask(0o077) try: with open('token.txt', 'w') as f: f.write(token) finally: os.umask(old)
Change owner by name (Unix)
shutil.chown accepts user and group names; os.chown wants numeric ids.
import shutil shutil.chown('/srv/app/data', user='www-data', group='www-data')
Examples
1. Remove the write permission
import os
import stat
open('ro.txt', 'w').close()
os.chmod('ro.txt', 0o444)
writable = bool(os.stat('ro.txt').st_mode & stat.S_IWUSR)
os.chmod('ro.txt', 0o644)
writable
Returns
False2. Writing a read-only file fails
import os
open('ro.txt', 'w').close()
os.chmod('ro.txt', 0o444)
try:
open('ro.txt', 'w')
except OSError as e:
result = (type(e).__name__, e.errno)
finally:
os.chmod('ro.txt', 0o644)
result
Returns
('PermissionError', 13)3. fchmod through a descriptor
import os
import stat
open('f.txt', 'w').close()
fd = os.open('f.txt', os.O_RDWR)
try:
os.fchmod(fd, 0o444)
ro = not os.stat('f.txt').st_mode & stat.S_IWUSR
os.fchmod(fd, 0o644)
finally:
os.close(fd)
ro
Returns
True4. Read a mode as text
import stat
(stat.filemode(stat.S_IFREG | 0o755), stat.filemode(stat.S_IFDIR | 0o700))
Returns
('-rwxr-xr-x', 'drwx------')5. What a umask of 0o022 leaves
(oct(0o666 & ~0o022), oct(0o777 & ~0o022))
Returns
('0o644', '0o755')6. umask returns the old mask
import os
old = os.umask(0o077)
try:
pass
finally:
os.umask(old)
type(old).__name__
Returns
'int'7. Missing file
import os
try:
os.chmod('ghost.txt', 0o644)
except OSError as e:
result = type(e).__name__
result
Returns
'FileNotFoundError'Pitfalls
1. Writing the mode in decimal
644 is decimal (0o1204), which sets write-for-owner, read-for-others and the sticky bit. Permission modes are octal.
644
import stat stat.filemode(stat.S_IFREG | 644)
'--w----r-T'
0o644
import stat stat.filemode(stat.S_IFREG | 0o644)
'-rw-r--r--'
2. Replacing the mode when you meant to add a bit
chmod sets the whole mode. Read st_mode and OR the new bit in to keep the others.
only S_IRUSR
import os import stat open('f.txt', 'w').close() os.chmod('f.txt', stat.S_IRUSR) writable = bool(os.stat('f.txt').st_mode & stat.S_IWUSR) os.chmod('f.txt', 0o644) writable
False
OR with st_mode
import os import stat open('f.txt', 'w').close() os.chmod('f.txt', os.stat('f.txt').st_mode | stat.S_IRUSR) bool(os.stat('f.txt').st_mode & stat.S_IWUSR)
True
When to use
Use it
- Making scripts executable, keys private (0o600), files read-only
- Setting a private umask around code that creates sensitive files
- Changing ownership in install or deploy scripts running as root (Unix)
Reach for something else
- Windows ACLs → not reachable through chmod (icacls or pywin32)
- Owner by user name → shutil.chown
- Only the mode at creation time → os.open(path, flags, mode) / os.mkdir(path, mode)
Notes
CPython impl
Modules/posixmodule.c. fchmod(fd, m) is chmod(fd, m), lchmod(p, m) is chmod(p, m, follow_symlinks=False), fchown and lchown likewise for chown
Availability
chmod, umask: Unix and Windows. fchmod: Unix, Windows (3.13+). lchmod: Unix, Windows (3.13+), not Linux, not OpenBSD (it is in os.__all__ on Windows 3.13 and missing on Linux). chown, fchown, lchown: Unix only
Windows chmod
Only the read-only flag is set or cleared (stat.S_IWRITE / S_IREAD); the mode then reads back as 0o444 or 0o666. Execute, group and other bits are ignored. fchmod needs a descriptor opened for writing there: on a read-only descriptor it raised PermissionError on Windows 3.13 but worked on Linux
Windows umask
Only the 0o600 bits of the mask are kept: os.umask(0o022) stores 0 and os.umask(0o777) stores 0o600 (verified on Windows CPython 3.13)
root
On Linux, root bypasses the permission bits, so a 0o444 file is still writable by root
FAQ
Permission bits are an OS feature: Windows only honours the read-only flag, and on Unix the result also depends on the owner and the umask. The examples only show changes that read back the same on Linux and Windows, and they restore what they change.