os, sys and the Runtime Environment

sys is about the running interpreter - arguments, streams, paths and exit. os is about the operating system - environment variables, processes and the filesystem.

sys: the interpreter itself

import sys

print(sys.version)                  # the full version string
print(sys.version_info)             # a comparable tuple
print(sys.platform)                 # 'win32', 'linux' or 'darwin'
print(sys.executable)               # the path to the python binary
print(sys.maxsize)                  # the largest normal integer index

if sys.version_info >= (3, 10):
    print("structural pattern matching is available")

Command line arguments

import sys

print(sys.argv)          # a list; argv[0] is the script name
$ python report.py data.csv --verbose
['report.py', 'data.csv', '--verbose']
import sys


def main():
    args = sys.argv[1:]
    if not args:
        print("usage: report.py FILE [--verbose]", file=sys.stderr)
        return 2
    path = args[0]
    verbose = "--verbose" in args
    print(f"processing {path}, verbose={verbose}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

For anything beyond two or three arguments, use argparse, which has its own note in this path.

The standard streams

import sys

print("normal output")                        # goes to stdout
print("a problem occurred", file=sys.stderr)  # goes to stderr

sys.stdout.write("no newline added\n")
sys.stderr.write("error text\n")

for line in sys.stdin:                        # read piped input
    print(line.rstrip().upper())
$ python upper.py < input.txt          # stdin from a file
$ cat input.txt | python upper.py      # stdin from a pipe
$ python report.py > out.txt           # stdout to a file, errors still shown
$ python report.py 2> errors.txt       # stderr to a file

Results go to stdout, problems go to stderr. Keeping them separate is what lets a user pipe your output into another program while still seeing the warnings.

Exiting

import sys

sys.exit(0)                     # success
sys.exit(1)                     # failure
sys.exit("fatal: no input")     # prints to stderr, exits with 1

The module search path

import sys

for entry in sys.path:
    print(entry)

print(len(sys.modules))                 # modules already loaded
print("json" in sys.modules)

print(sys.getrecursionlimit())          # 1000
print(sys.getsizeof([1, 2, 3]))         # bytes used by the object itself

os: the operating system

Environment variables

import os

print(os.environ.get("PATH"))
print(os.environ.get("HOME") or os.environ.get("USERPROFILE"))

# Reading with a default - never assume a variable is set
debug = os.environ.get("DEBUG", "0") == "1"
port = int(os.environ.get("PORT", "8000"))

os.environ["APP_MODE"] = "test"         # only for this process and its children
print(os.environ["APP_MODE"])

print("DATABASE_URL" in os.environ)
Environment variables are the standard way to keep secrets and per machine settings out of source code. Read them with .get and a sensible default, and never commit a real key to a repository.
import os
import sys


def get_required(name):
    value = os.environ.get(name)
    if value is None:
        sys.exit(f"fatal: the environment variable {name} is not set")
    return value

Working directory and process

import os

print(os.getcwd())                # the current working directory
os.chdir("/tmp")                  # change it - affects every relative path

print(os.getpid())                # the process id
print(os.cpu_count())             # logical processors available
print(os.name)                    # 'posix' or 'nt'

Changing the working directory affects the whole process and every relative path in it. Prefer building absolute paths from __file__ over calling chdir.

Filesystem work

import os

print(os.listdir("."))
os.makedirs("data/reports", exist_ok=True)
os.rename("old.txt", "new.txt")
os.replace("temp.txt", "final.txt")     # atomic, overwrites
os.remove("scratch.txt")
os.rmdir("empty_folder")

for root, folders, files in os.walk("project"):
    print(root, len(folders), len(files))

For new code, prefer pathlib, covered in the file handling notes. os remains the right choice for os.replace, os.walk when you want the three way split, and everything to do with the process itself.

Running another program

import subprocess

result = subprocess.run(
    ["python", "--version"],
    capture_output=True,
    text=True,
    check=False,
)

print(result.returncode)
print(result.stdout.strip())
print(result.stderr.strip())
# Safe: the arguments are a list, so the shell never parses them
subprocess.run(["ls", user_supplied_name])

# Dangerous: shell=True lets the input become shell syntax
# subprocess.run(f"ls {user_supplied_name}", shell=True)

Pass a list of arguments and leave shell=False, which is the default. With shell=True, a name containing ; rm -rf / becomes a second command. The security note covers this in more detail.

platform

import platform

print(platform.system())            # Windows, Linux, Darwin
print(platform.release())
print(platform.machine())           # AMD64, x86_64, arm64
print(platform.python_version())
print(platform.node())              # the machine name

Writing portable code

import os
import sys
from pathlib import Path

# Paths: let pathlib choose the separator
config = Path.home() / ".config" / "notesapp" / "settings.txt"

# Line endings: text mode handles them
with open("out.txt", "w", encoding="utf-8") as handle:
    handle.write("line\n")

# Branch on the platform only when there is a genuine difference
if sys.platform == "win32":
    clear_command = "cls"
else:
    clear_command = "clear"

print(os.linesep.encode())          # what this platform uses on disk
print(os.sep)                       # the path separator

A worked example

"""Report on the environment this program is running in."""

import os
import platform
import sys
from pathlib import Path


def report():
    print(f"{'Python':<16}{platform.python_version()}")
    print(f"{'Executable':<16}{sys.executable}")
    print(f"{'Platform':<16}{platform.system()} {platform.release()}")
    print(f"{'Machine':<16}{platform.machine()}")
    print(f"{'CPUs':<16}{os.cpu_count()}")
    print(f"{'Working dir':<16}{Path.cwd()}")
    print(f"{'Script':<16}{Path(__file__).resolve()}")
    print(f"{'Arguments':<16}{sys.argv[1:] or 'none'}")

    interesting = ["PATH", "HOME", "USERPROFILE", "PYTHONPATH", "DEBUG"]
    for name in interesting:
        value = os.environ.get(name)
        if value:
            shown = value if len(value) < 40 else value[:37] + "..."
            print(f"{name:<16}{shown}")


if __name__ == "__main__":
    report()

Common mistakes

  • Assuming sys.argv[0] is the first real argument. It is the script name.
  • Reading an environment variable with os.environ["X"] and getting KeyError on another machine.
  • Writing errors to stdout, so they end up in a redirected output file.
  • Using shell=True with input you did not write.
  • Calling os.chdir and breaking every relative path elsewhere in the program.
  • Hard coding / or \\ in paths.

Best practices

  • Read configuration from environment variables with defaults; never commit secrets.
  • Send results to stdout and diagnostics to stderr.
  • Return an exit code from main() and pass it to sys.exit.
  • Use subprocess.run with a list of arguments.
  • Use pathlib for paths and sys.platform only for genuine platform differences.

Practice

  1. Write a script that reads a filename from sys.argv and exits with a clear code and message when it is missing.
  2. Write a program that reads lines from standard input and reports word counts, so it works in a pipe.
  3. Read three settings from environment variables, each with a default, and print the resulting configuration.
  4. Print a report of the current platform, Python version and CPU count.
  5. Run an external command with subprocess.run and handle a non zero return code.

Conclusion

sys is the interpreter: arguments, streams, exit codes and the import path. os is the machine: environment variables, the working directory, processes and files. Together they turn a Python file into a program that behaves properly on a command line.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.