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
- Command line arguments
- The standard streams
- Exiting
- The module search path
- os: the operating system
- Environment variables
- Working directory and process
- Filesystem work
- Running another program
- platform
- Writing portable code
- A worked example
- Common mistakes
- Best practices
- Practice
- Conclusion
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
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 fileResults 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 1The 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 itselfos: 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 valueWorking 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 nameWriting 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 separatorA 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 gettingKeyErroron another machine. - Writing errors to
stdout, so they end up in a redirected output file. - Using
shell=Truewith input you did not write. - Calling
os.chdirand 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
stdoutand diagnostics tostderr. - Return an exit code from
main()and pass it tosys.exit. - Use
subprocess.runwith a list of arguments. - Use
pathlibfor paths andsys.platformonly for genuine platform differences.
Practice
- Write a script that reads a filename from
sys.argvand exits with a clear code and message when it is missing. - Write a program that reads lines from standard input and reports word counts, so it works in a pipe.
- Read three settings from environment variables, each with a default, and print the resulting configuration.
- Print a report of the current platform, Python version and CPU count.
- Run an external command with
subprocess.runand 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.