Python Module 16 – Practical Python: text, CLIs, web, automation and security
Working with the OS: os, sys, shutil, platform
Read environment variables, open files relative to the script, stop with an exit status, find programs and identify the system with os, sys and platform.
What you will learn
- Inspect the interpreter, the operating system and the process with sys, platform and os
- Read settings from environment variables with defaults, and open files relative to the script
- Report success or failure to the caller with sys.exit() and an exit status
- Write path and newline handling that behaves the same on Windows, macOS and Linux
- Predict what a parent process sees when its child changes the environment or exits, and check disk space first
Before you start
What you’ll learn · Before you start
What you will learn
- Inspect the interpreter, the operating system and the process with sys, platform and os
- Read settings from environment variables with defaults, and open files relative to the script
- Report success or failure to the caller with sys.exit() and an exit status
- Write path and newline handling that behaves the same on Windows, macOS and Linux
- Predict what a parent process sees when its child changes the environment or exits, and check disk space first
Before you start
On this page
A report script works in your terminal and fails every night under the scheduler: the setting it needs is not set
there, prices.csv is not in the folder it starts in, and the job still reports success, because it exits with
status 0 whatever happened. Each of those failures is about how a process meets the computer it runs on.
Four standard modules handle that meeting. os describes and changes the process: its environment variables in
os.environ, its working directory and the CPUs it may use. sys describes the interpreter: the arguments in
sys.argv, the platform in sys.platform, the version in sys.version_info, and sys.exit() to stop with an exit
status. shutil copies, moves and deletes whole files and folders, and finds programs with shutil.which().
platform names the operating system and processor in words a person reads.
A process starts with arguments, an environment and a folder
Every program, a Python script included, is started by another one: a shell, a scheduler or a test runner. The parent passes three things, the arguments, a copy of its environment variables and its working directory, and the child hands back one number when it ends, its exit status: The small number a program hands back to whatever started it: 0 for success, anything else for failure. Shells, CI jobs and schedulers read it; by convention 2 means a wrong command line and 1 any other error. More in “Designing error handling for real programs”:
What a Python process gets when it starts, and what it hands back
Text description of the diagram
1. A parent program, such as a shell or a scheduler, starts python3 report.py --month 3. 2. It hands the new process three things: the program and its arguments, a copy of its own environment variables, and its own working directory. 3. Inside Python they appear as sys.argv (the script name and its arguments), os.environ (a copy of the environment, read when the os module is first imported) and os.getcwd() (the folder that relative paths start from). Changes the process makes to them stay in the process and in the processes it starts; the parent never sees them. 4. sys.exit(n) ends the process, and the parent reads n as its exit status: 0 means success.
Each of the three is the child’s own copy, so a script can change them freely and the parent never sees the change. Step through one run of a report script and watch which changes stay inside it:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- The shell starts Python. In the folder /home/asha, a shell runs python3 report.py --month 3. The shell's own environment holds SHOP_MODE=staging.
- A copy of the environment. The new process receives a copy of the shell's environment variables, so os.environ['SHOP_MODE'] is 'staging' inside the script too.
- And the working folder. It also starts in the shell's working directory, /home/asha. A relative name such as prices.csv is looked up there, wherever the script itself is stored.
- The script changes its copy. report.py assigns os.environ['SHOP_MODE'] = 'test'. Only its own copy changes; the shell still holds staging.
- Its child gets the change. The script starts another program with subprocess. That program receives a copy of the script's environment as it is now, so it sees 'test'.
- The child reports back. The child finishes and hands back one number, its exit status, 0 for success, which subprocess.run() returns in returncode.
- The script moves folder. os.chdir('reports') moves the script to /home/asha/reports. The shell's working directory does not move.
- The script exits. report.py ends with sys.exit(0). The shell receives that number, 0, which it shows in $?, and its environment and folder are as they were.
Only the exit status travelled back. The four modules divide the work like this:
| Module | The question it answers | Calls you use most |
|---|---|---|
os |
What is this process’s environment and working directory? | os.environ, os.getcwd(), os.getpid(), os.process_cpu_count() |
sys |
Which interpreter runs, with which arguments, and how does it stop? | sys.argv, sys.exit(), sys.platform, sys.version_info |
shutil |
How do I copy, move or delete whole trees, and where is a program? | shutil.copy2(), shutil.move(), shutil.rmtree(), shutil.which() |
platform |
Which operating system and processor, in words? | platform.system(), platform.machine(), platform.python_version() |
Checkpoint
Check: What a process gets and hands back
sys, platform and os say what the script runs on
"""A short report on the interpreter, the operating system and the process running this script."""
import os
import platform
import sys
print("Python", platform.python_version(), platform.python_implementation())
print("sys.version_info >= (3, 14):", sys.version_info >= (3, 14))
print("sys.platform:", sys.platform)
print("platform.system():", platform.system())
print("platform.machine():", platform.machine())
print("os.name:", os.name)
print("CPUs this process may use:", os.process_cpu_count())
print("arguments after the script name:", sys.argv[1:]) Output
Python 3.14.8 CPython sys.version_info >= (3, 14): True sys.platform: darwin platform.system(): Darwin platform.machine(): arm64 os.name: posix CPUs this process may use: 18 arguments after the script name: []
This output changes from run to run: the platform, the processor and the number of CPUs depend on the computer that runs it
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 system_report.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Version note
In the browser, Python runs on WebAssembly, so the same program reports emscripten and wasm32 there, and a single CPU.
In your browser, Pyodide 314.0.7 (CPython 3.14.2) prints:
Python 3.14.2 CPython sys.version_info >= (3, 14): True sys.platform: emscripten platform.system(): Emscripten platform.machine(): wasm32 os.name: posix CPUs this process may use: 1 arguments after the script name: []
Compare sys.version_info with a tuple, sys.version_info >= (3, 14), never the sys.version string. Code that
branches on the system should test sys.platform: "win32" on Windows, "darwin" on macOS, "linux" on Linux and
"emscripten" in the browser. platform.system() says the same in words for reports and logs. os.process_cpu_count()
counts the CPUs this process may use, which can be fewer than os.cpu_count() reports for the whole machine.
sys.argv[0] is the script’s own name and the arguments follow it, which is why the report prints sys.argv[1:].
sys.executable is the interpreter running the script: start helper scripts with it, so that they run on the same
Python and in the same virtual environment.
Checkpoint
Check: sys, platform and os on the running system
Environment variables arrive as strings
Settings that change from one computer to another, such as a mode, a number of workers or an API token, usually come from environment variable: A name with a text value that a process receives from the program that started it, such as APP_PORT=9000. Python reads them from os.environ, always as str, and every process it starts gets a copy of them. More in “Config files, environment variables and secrets”. This program sets two of them the way a shell would, then reads four settings. Step through it, or press Edit and run it with other values:
Program · settings_from_env.py
"""Settings from environment variables."""
import os
os.environ["SHOP_MODE"] = "staging"
os.environ["SHOP_WORKERS"] = "8"
mode = os.environ.get("SHOP_MODE", "production")
workers = int(os.environ.get("SHOP_WORKERS", "4"))
flag = os.environ.get("SHOP_DEBUG", "")
debug = flag.lower() in {"1", "true", "yes"}
print(f"mode={mode!r} workers={workers} debug={debug}")
try:
token = os.environ["SHOP_API_TOKEN"]
except KeyError as missing:
print("not set:", missing)
print("SHOP_DEBUG" in os.environ, os.getenv("SHOP_DEBUG"))
del os.environ["SHOP_MODE"]
print(os.environ.get("SHOP_MODE", "production")) Variables
- changed in this step
Printed so far
Nothing printed yetmode='staging' workers=8 debug=False not set: 'SHOP_API_TOKEN' False None production
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 17 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
"""Settings from environment variables."""runs. - Line 2.
import osloadsos. - Line 4.
os.environ["SHOP_MODE"] = "staging"runs. - Line 5.
os.environ["SHOP_WORKERS"] = "8"runs. Both values are stored as strings, as every environment variable is. A shell that ran export SHOP_WORKERS=8 would have stored the same text. - Line 7.
mode = os.environ.get("SHOP_MODE", "production")setsmodeto'staging'. Key point .get() returns the value when the variable is set, and the default only when it is missing, so mode is 'staging'. - Line 8.
workers = int(os.environ.get("SHOP_WORKERS", "4"))setsworkersto8. int() turns the text '8' into the number 8. Without it, workers would be a string, and workers + 1 would raise TypeError. - Line 9.
flag = os.environ.get("SHOP_DEBUG", "")setsflagto''. - Line 10.
debug = flag.lower() in {"1", "true", "yes"}setsdebugtoFalse. SHOP_DEBUG is not set, so flag holds the default, an empty string. Only the listed spellings count as true, so debug is False. - Line 11.
print(f"mode={mode!r} workers={workers} debug={debug}")printsmode='staging' workers=8 debug=False. - Line 13.
try:runs. - Line 14.
token = os.environ["SHOP_API_TOKEN"]runs. Key point Square brackets raise KeyError for a missing variable, which is the right behaviour for a setting such as an API token that must exist. - Line 15. Line 15 runs.
- Line 16.
print("not set:", missing)printsnot set: 'SHOP_API_TOKEN'.missingno longer exists. - Line 18.
print("SHOP_DEBUG" in os.environ, os.getenv("SHOP_DEBUG"))printsFalse None. - Line 19.
del os.environ["SHOP_MODE"]runs. del unsets SHOP_MODE for this process and for the programs it starts. The shell that started the script keeps its own copy. - Line 20.
print(os.environ.get("SHOP_MODE", "production"))printsproduction. That was the last line, so the program ends. Every setting arrived as text, and every missing one as a default, None or a KeyError. Nothing outside this process changed.
os.environ behaves like a dictionary of strings. .get(name, default) gives a default when the variable is not set,
while os.environ[name] raises KeyError, the right choice for a setting that must exist. Every value is text, so
numbers and flags need converting. Changes made through os.environ reach this process and the programs it starts,
never the shell that started it. Secrets such as API tokens belong there too, out of the code and off the
command line, which other users of a computer can often read; every program your script starts receives them.
Checkpoint
Check: Settings from environment variables
Relative paths start in the working directory, not beside the script
"""A file opened by a relative name is looked up in the working directory, not next to the script."""
import os
from pathlib import Path
HERE = Path(__file__).resolve().parent # the folder this script lives in
(HERE / "prices.csv").write_text("item,paise\ntea,1500\n", encoding="utf-8")
(HERE / "reports").mkdir(exist_ok=True)
def read_relative():
with open("prices.csv", encoding="utf-8") as f: # relative to os.getcwd()
return f.readline().strip()
def read_beside_script():
return (HERE / "prices.csv").read_text(encoding="utf-8").splitlines()[0]
print("working directory is the script's folder:", Path.cwd() == HERE)
print("relative name:", read_relative())
os.chdir(HERE / "reports") # what a scheduler, an IDE or a cd before the command does
print("working directory is the script's folder:", Path.cwd() == HERE)
try:
print("relative name:", read_relative())
except FileNotFoundError as e:
print("relative name: FileNotFoundError:", e.strerror)
print("next to the script:", read_beside_script()) Output
working directory is the script's folder: True relative name: item,paise working directory is the script's folder: False relative name: FileNotFoundError: No such file or directory next to the script: item,paise
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cwd_trap.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
A relative name such as "prices.csv" is looked up in the working directory, which is wherever the caller happened
to be. A scheduler, an editor’s Run button or a cd before the command can each change it, and the script breaks.
Path(__file__).resolve().parent is the script’s own folder, so build paths to your own files from it.
The same paths feed shutil. shutil.copy2() copies a file with its timestamps, shutil.copytree() copies a whole
folder and shutil.move() moves a file or folder, even to another disk. shutil.rmtree() deletes a folder and
everything in it, with no undo and no recycle bin, so check the path it receives before calling it.
Checkpoint
Check: Relative paths and the working directory
sys.exit() ends the script with a status the caller reads
"""sys.exit() with a message: the message goes to standard error and the exit status is 1."""
import sys
def require_python(minimum):
if sys.version_info < minimum:
wanted = ".".join(str(n) for n in minimum)
running = f"{sys.version_info.major}.{sys.version_info.minor}"
sys.exit(f"This tool needs Python {wanted} or newer; this is Python {running}.")
try:
require_python((3, 10))
print("Python 3.10 or newer: carry on")
require_python((3, 15))
print("this line is never reached")
finally:
print("the finally block still ran: sys.exit() raises SystemExit") Output (exit status 1)
Python 3.10 or newer: carry on the finally block still ran: sys.exit() raises SystemExit
Printed as an error (standard error)
This tool needs Python 3.15 or newer; this is Python 3.14.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 require_python.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
sys.exit() raises SystemExit, so finally blocks and with statements still clean up, as the line from the
finally block shows. An integer becomes the exit status: 0 means success, any other number failure, and 2
conventionally a mistake on the command line, which is what argparse uses. A string is printed on standard error and gives status 1, which
shells, CI jobs and schedulers read as failure. That is how the nightly job of the opening should end when its
setting is missing: with a message that names the setting and a status that makes the scheduler report a failure,
instead of a status of 0 and an empty report.
Checkpoint
Check: sys.exit() and the exit status
Check yourself
Each question below checks one of the five ideas about processes, and the session starts with the idea you know least well.
Practice that adapts to you
Practise this lesson
10 questions about 11 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Scripts that behave the same on Windows, macOS and Linux Pro: opens with a Pro, Premium or Ultimate pass
Three habits cause many portability bugs: joining paths by hand, splitting PATH on a colon and writing the line
ending yourself. This part reproduces each one on any computer, then finds programs and tells the platforms apart
with the right call for the job.
In this part:
- Windows path rules, reproduced on any computer
- Line endings: when text mode writes one for you
- Finding programs and telling platforms apart
- A portability checklist, habit by habit
3 runnable examples (Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Environment changes, exit statuses and disk space, measured Pro: opens with a Pro, Premium or Ultimate pass
A script talks to the processes around it through the environment and the exit status, and both have rules that
surprise people. This part shows what a child process inherits, why os.environ can be out of date, which exit
statuses a parent really receives, and how to check free space before a large job.
In this part:
- What a child inherits, and what os.environ can miss
- Exit statuses a parent really sees
- Checking free space before a large job, with numbers
- Exercise: a system summary that tests can fake
- A system summary that tests can fake
4 runnable examples (Python) · 1 exercise with sample tests · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What is the difference between sys.platform and platform.system(), and how should a script check which Python version runs it?
Both describe the operating system, for different readers. sys.platform is an identifier meant for decisions in code: "win32" on Windows, "darwin" on macOS, "linux" on Linux and "emscripten" in the browser. platform.system() returns a name for people, such as "Windows", "Darwin" or "Linux", which suits reports and logs. So code branches on sys.platform, and a report prints platform.system(). For the version, compare sys.version_info, a tuple of numbers, with a tuple: sys.version_info >= (3, 14). Comparing the sys.version string fails, because strings compare character by character and "3.9" sorts after "3.14". Both checks belong at the start of a script, so that it stops with a clear message through sys.exit() on a system or a version it cannot support.
-
Mid to senior
A worker thread of a batch job calls sys.exit(1) when it finds corrupt input, yet the job's exit status is 0 and the scheduler reports success. Explain why, and how you would make the job fail correctly.
-
Mid to senior
You review a deployment script written on a Mac that must also run on Windows build servers. It joins paths with f-strings, splits PATH on ":", writes os.linesep to text files and calls rm -rf through a shell. What do you flag, and how do you test the fixes without a Windows machine?
-
Mid to senior
A service loads a C extension that sets TZ with setenv() at start-up. Child processes started by the service see the new TZ, but os.environ["TZ"] still shows the old value. Why, and what do you change?
Key takeaways
- A process starts with arguments, a copy of its parent’s environment and a working directory, and hands back one number, its exit status.
- Test
sys.platformin code, comparesys.version_infowith tuples, and keepplatformfor reports. os.environholds strings: use.get(name, default)for optional settings,[name]for required ones, and convert.- A relative name starts in the working directory, so build paths from
Path(__file__).resolve().parent. sys.exit("message")prints the message on standard error and exits with status 1, afterfinallyblocks run.
References
- os: miscellaneous operating system interfaces (Python Software Foundation)
- os.environ (Python Software Foundation)
- os.reload_environ() (Python Software Foundation)
- sys.exit() (Python Software Foundation)
- sys.platform (Python Software Foundation)
- platform: access to underlying platform identifying data (Python Software Foundation)
- shutil.which() (Python Software Foundation)
- shutil.disk_usage() (Python Software Foundation)
- pathlib: pure paths (Python Software Foundation)
- Built-in Functions: open() and its newline parameter (Python Software Foundation)
- threading.excepthook() (Python Software Foundation)
- subprocess: the Popen constructor and env (Python Software Foundation)
- POSIX: exit() (The Open Group and IEEE)
- POSIX shell command language: exit status for commands (The Open Group and IEEE)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress