Bobobk

Practical Python, Linux and automation guides

Python File Permissions on Linux: umask, Mode 600, and No Overwrites

2026-10-28 Technology
Python File Permissions on Linux: umask, Mode 600, and No Overwrites
On this page

A Python report intended only for you appears with mode 644 after moving to another terminal or automation workflow. The requested creation mode and inherited umask often explain the difference; Python is not changing permissions at random.

This beginner-to-intermediate tutorial reproduces 022 versus 077, then creates a new JSON file without granting group or other-user permissions from the start. It uses dummy ready data, reads no real credentials, and changes no server or scheduler settings.

1. Method: a whole task or one file

  • Restrictive defaults for a whole task: set umask 077 in a subshell without changing the current terminal.
  • Restrict one new file: request 0o600 through an open() opener and use x mode to refuse overwrites. The complete script follows.

Without a directory default ACL, creation permissions follow requested_mode & ~umask, not decimal subtraction. Ordinary text creation usually requests 666: mask 022 produces 644, while 077 produces 600. Mode 600 grants owner read/write, with no group or other-user permissions. It is not encryption. Linux umask manual

Permissions do not replace directory isolation, backups, or access management. Use a trusted directory and trusted parents, with no concurrent path replacement. Do not handle real sensitive data in a shared writable directory.

2. Setup and reproduce the difference

Use Linux and an existing Python 3.9 or newer installation; no packages are required. Commands work in bash or zsh and were tested on Linux with Python 3.11.4. Keep the same terminal and working directory throughout; choose a new directory name when repeating the experiment.

  1. Show the version and current shell mask. Running umask alone only displays it:
python3 --version
mkdir umask-demo
cd umask-demo
umask
  1. Create files in two subshells, then inspect their actual modes:
(
  umask 022
  python3 -c "with open('ordinary-022.txt', 'x', encoding='utf-8') as f: f.write('demo\n')"
)
(
  umask 077
  python3 -c "with open('ordinary-077.txt', 'x', encoding='utf-8') as f: f.write('demo\n')"
)
python3 - <<'PY_MODES'
from pathlib import Path
import stat
for name in ("ordinary-022.txt", "ordinary-077.txt"):
    mode = stat.S_IMODE(Path(name).stat().st_mode)
    print(f"{name}: {mode:03o}")
PY_MODES

Expected output in an ordinary local directory without a default ACL:

ordinary-022.txt: 644
ordinary-077.txt: 600

The settings inside parentheses end with each subshell. If results differ, inspect the parent’s default ACL and filesystem behavior instead of running chmod -R on the directory. Default ACLs change creation-mode calculation; see the Linux manual linked above.

3. Complete implementation: create, never overwrite

Save this as write_private.py in the current directory:

"""Create a new private demo JSON file on Linux. Python 3.9+, standard library."""
import argparse
import json
import os
from pathlib import Path
import stat
import sys


def private_opener(path, flags):
    return os.open(path, flags, 0o600)


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("output", type=Path, help="new file in a trusted directory")
    args = parser.parse_args()
    try:
        with open(args.output, "x", encoding="utf-8", newline="\n",
                  opener=private_opener) as handle:
            json.dump({"status": "ready"}, handle)
            handle.write("\n")
            mode = stat.S_IMODE(os.fstat(handle.fileno()).st_mode)
    except FileExistsError:
        print("ERROR: output already exists; not changed", file=sys.stderr)
        return 1
    except OSError as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(json.dumps({"file": str(args.output), "mode": f"{mode:03o}"}))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

The open() opener parameter receives a path and flags and returns a file descriptor. This opener preserves the flags supplied by x mode and sets the underlying os.open() creation mode to 0o600. A mask can remove additional owner permissions, so the result is 600 or stricter, not a forced guarantee of owner read/write access.

POSIX exclusive creation refuses existing paths, including a symlink at the final component. It does not protect replaceable parent directories. Linux open manual

The script never calls os.umask(), avoiding a temporary process-wide creation-policy change. If the directory cannot be written at all, start with the directory write-permission probe. Restricting creation mode cannot make an unwritable directory writable.

4. Usage: request private permissions even with a permissive mask

  1. Show help and run in a subshell using umask 000. Repeat the same path to verify refusal:
python3 write_private.py --help
(
  umask 000
  python3 write_private.py private.json
)
python3 -m json.tool private.json
if python3 write_private.py private.json; then
  printf 'Unexpected success\n'
else
  printf 'Existing file refused\n'
fi

The first invocation writes this to stdout:

{"file": "private.json", "mode": "600"}

json.tool should display the formatted equivalent of {"status": "ready"}. The second invocation writes ERROR: output already exists; not changed to stderr, followed by the shell’s Existing file refused message.

  1. Handle exit codes: 0 means creation, writing, and closing succeeded; 1 means an existing path or another file-operation failure; 2 means invalid command arguments. Only success writes one JSON line containing path and mode to stdout, never the file’s contents.

This is not atomic publication. A write or close failure can leave an empty or incomplete newly created file. Handle nonzero exit codes rather than treating existence as success. To update existing output, continue with atomic JSON saves and explicitly decide the replacement file’s permissions.

5. Validation: masks and overwrite-refusal branches

This self-check runs in a separate temporary subdirectory and leaves the earlier files alone. Its umask= argument sets only the child process; see the Python subprocess documentation.

python3 - <<'PY_CHECK'
import json
from pathlib import Path
import stat
import subprocess
import sys
import tempfile

script = Path("write_private.py").resolve()
with tempfile.TemporaryDirectory(prefix="mode-check-", dir=".") as folder:
    root = Path(folder)
    def run(path, mask=0o022):
        return subprocess.run(
            [sys.executable, str(script), str(path)],
            umask=mask, capture_output=True, text=True, check=False,
        )

    for mask, expected in [(0o000, 0o600), (0o022, 0o600),
                           (0o077, 0o600), (0o277, 0o400)]:
        path = root / f"private-{mask:03o}.json"
        result = run(path, mask)
        assert result.returncode == 0 and not result.stderr, result
        assert stat.S_IMODE(path.stat().st_mode) == expected
        assert json.loads(result.stdout)["mode"] == f"{expected:03o}"
        assert json.loads(path.read_text()) == {"status": "ready"}

    existing = root / "existing.json"
    existing.write_bytes(b"keep\n")
    existing.chmod(0o644)
    link = root / "link.json"
    link.symlink_to(existing.name)
    broken = root / "broken.json"
    broken.symlink_to("absent.json")
    for path in (existing, link, broken, root, root / "missing" / "file.json"):
        result = run(path)
        assert result.returncode == 1 and result.stdout == "", result
        assert result.stderr.startswith("ERROR:"), result
    assert existing.read_bytes() == b"keep\n"
    assert stat.S_IMODE(existing.stat().st_mode) == 0o644
    assert link.is_symlink() and broken.is_symlink()
    assert not (root / "absent.json").exists()
    result = subprocess.run([sys.executable, str(script)],
                            capture_output=True, text=True, check=False)
    assert result.returncode == 2 and not result.stdout and result.stderr
print("OK: masks, JSON, existing file, symlinks, directory, missing parent, arguments")
PY_CHECK

Expected output:

OK: masks, JSON, existing file, symlinks, directory, missing parent, arguments

The test checks both contents and modes, including preservation of an existing file’s bytes and 644 mode. Mask 277 also removes owner write permission, producing 400. The write descriptor opened during creation can still complete that write; subsequent opens are subject to file permissions. The temporary directory is cleaned on exit; see automatic temporary-directory cleanup for more examples.

6. Troubleshooting: do not hide problems by widening permissions

Symptom Concrete action
An old file stays 644 after switching to 077 The mask affects creation only. Check ownership and sharing requirements, then restrict that specific file rather than recursively changing a directory tree.
output already exists Choose a new output name or design explicit replacement after reviewing the old result. Do not delete an old file just to pass a check.
Permission denied Check the executing account, parent-directory access, and actual file mode using the earlier directory-probe tutorial. Do not jump to 777.
No such file or directory The script does not create parents. Create an output directory in a trusted location, then pass the complete path.
Results differ from the mode table Inspect default ACLs, mounts, and filesystem permission support. Reproduce in an ordinary local demo directory first.
A web server cannot read the output Private files and public Hugo assets need different policies. Grant only necessary read access to intentionally public output; keep sensitive files outside the website directory.

Demonstrate repairing an old mode only on the demo file created here:

python3 - <<'PY_FIX'
from pathlib import Path
import stat
path = Path("ordinary-022.txt")
path.chmod(0o600)
print(f"{path.name}: {stat.S_IMODE(path.stat().st_mode):03o}")
PY_FIX

Expect ordinary-022.txt: 600. This change is not controlled by the creation mask. Restricting real data later cannot revoke copies or reads that already happened while its permissions were broader.

7. Summary

Use a subshell’s umask 077 for restrictive defaults across a private task, or request 0o600 during exclusive creation for one output. Verify creation modes, overwrite refusal, and exit codes before adopting the pattern. Correct permissions do not prove the file’s contents are complete.

Tags

1024 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print probability prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sql sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode upsert urllib usdt utf-8 uv venv vhhbert vite wallet web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 迅雷解析

友情链接