Yesterday you found the log directory with pathlib. Today the log files are there, but your script needs to know where to look. The monitoring team changes the log root on different servers. How are you passing that in right now?
I hardcoded it. /var/log/services/ in the script itself. When Ahmad runs it on staging where logs live under /home/deploy/logs/, he edits the file. He always forgets to change it back. Last week I got a report built from staging data.
Classic hardcoded path problem. The fix is environment variables — values the OS holds for your process before the script even starts. os.environ reads them:
import os
log_root = os.environ.get("LOG_ROOT", "/var/log/services")
log_level = os.environ.get("LOG_LEVEL", "INFO")
debug = os.environ.get("DEBUG", "") != ""os.environ behaves exactly like a Python dict. .get() with a default — the same pattern you use on regular dicts — works here. The monitoring team sets LOG_ROOT=/home/deploy/logs before running the script, and it just works. No file to edit.
os.environ is just a dict? I've been using python-dotenv to read .env files. You're saying the OS already provides this?
python-dotenv loads a .env file into os.environ at startup. But the reading is always os.environ. On a real server, ops sets the variables directly — no .env file needed. Your script reads from the same place either way.
Okay, but when I run python3 analyze.py --service auth --date today, where do those arguments live? That's sys.argv, right?
sys.argv. You're already thinking about it correctly — it's a plain Python list:
import sys
# python3 analyze.py --service auth --date today
print(sys.argv)
# ['analyze.py', '--service', 'auth', '--date', 'today']
script_name = sys.argv[0] # 'analyze.py'
args = sys.argv[1:] # ['--service', 'auth', '--date', 'today']sys.argv[0] is always the script name. Everything after is the raw arguments as strings. It's a list — you can iterate it, index it, slice it.
So sys.argv is the intercom system. The user types something into the console, it travels through sys.argv, and my script picks up the call. I've been refusing to answer the intercom and using input() prompts instead.
input() works for interactive scripts. It breaks the moment you want to schedule the job with cron. A script that waits for keyboard input at 3 a.m. is not a good cron job. And there's one more sys function for production scripts:
import sys, os
if not os.environ.get("LOG_ROOT"):
print("Error: LOG_ROOT not set", file=sys.stderr)
sys.exit(1)sys.exit(1) stops the script and tells the calling process something went wrong. sys.stderr keeps error messages out of your data stream — when you redirect output to a file, only stdout goes to the file. Errors stay visible in the terminal.
sys.stderr is what I've been missing. I pipe the script's output and the error message ends up on line 1 of the CSV. I've been stripping it manually. I could have used file=sys.stderr the whole time.
The mental model: os.environ is for stable configuration that changes by deployment context. sys.argv is for per-invocation arguments. Environment is the thermostat — set once, controls everything. sys.argv is the intercom — you talk into it each time.
And today's problem simulates exactly this: parse KEY=VALUE strings into an env dict, parse argv for script_name and args, read LOG_LEVEL with a default, check for --debug in args. The tricky bit is split("=", 1) to handle values that contain = — like a database URL.
Good catch on the URL case. "DATABASE_URL=postgres://host/db?ssl=true".split("=") gives you four pieces. split("=", 1) gives you exactly two. Tomorrow the ops team needs to move six months of archived logs to cold storage — you're going to meet shutil, Python's moving-company module.
os and sys are the two standard library modules that connect a Python script to its operating environment. They have different scopes: os mediates between Python and the operating system — files, processes, environment variables, working directory. sys mediates between Python and the Python interpreter itself — the module search path, the running version, the current exception, and most importantly sys.argv and sys.exit().
The twelve-factor app methodology codifies what ops engineers have known for decades: configuration that changes between environments — database URLs, log levels, feature flags, API keys — belongs in the environment, not in the code. os.environ exposes the entire process environment as a dict-like object. os.environ.get("KEY", "default") is the canonical read pattern. os.environ["KEY"] = "value" sets a variable for the current process and any child processes it spawns.
The most common mistake is treating the absence of a variable as an error when it should have a sensible default. os.environ.get("LOG_LEVEL", "INFO") is almost always preferable to os.environ["LOG_LEVEL"], which raises KeyError when the variable is not set.
sys.argv is the unprocessed command line as a list of strings. Index 0 is always the script name. Everything after is what the user typed, split on whitespace by the shell. sys.argv is not argument parsing — for that, Week 4 introduces argparse. sys.argv is the raw material. Direct sys.argv access is appropriate for very simple scripts; anything with flags, types, or help text deserves argparse.
Python scripts have three standard streams: sys.stdin (input), sys.stdout (output), sys.stderr (diagnostic). The critical operational distinction: when a user redirects output with python3 script.py > results.csv, only sys.stdout goes to the file. sys.stderr stays on the terminal. Printing errors to sys.stderr with print("Error: ...", file=sys.stderr) ensures they remain visible even when output is piped or redirected.
sys.exit(0) signals success. sys.exit(1) (or any non-zero integer) signals failure. Shell scripts, cron jobs, CI pipelines, and monitoring systems all check exit codes. A script that encounters a fatal error and exits with 0 tells its callers "everything is fine" — which is worse than crashing. sys.exit(1) with an informative message to sys.stderr is the correct pattern for fatal configuration errors.
Sign up to write and run code in this lesson.
Yesterday you found the log directory with pathlib. Today the log files are there, but your script needs to know where to look. The monitoring team changes the log root on different servers. How are you passing that in right now?
I hardcoded it. /var/log/services/ in the script itself. When Ahmad runs it on staging where logs live under /home/deploy/logs/, he edits the file. He always forgets to change it back. Last week I got a report built from staging data.
Classic hardcoded path problem. The fix is environment variables — values the OS holds for your process before the script even starts. os.environ reads them:
import os
log_root = os.environ.get("LOG_ROOT", "/var/log/services")
log_level = os.environ.get("LOG_LEVEL", "INFO")
debug = os.environ.get("DEBUG", "") != ""os.environ behaves exactly like a Python dict. .get() with a default — the same pattern you use on regular dicts — works here. The monitoring team sets LOG_ROOT=/home/deploy/logs before running the script, and it just works. No file to edit.
os.environ is just a dict? I've been using python-dotenv to read .env files. You're saying the OS already provides this?
python-dotenv loads a .env file into os.environ at startup. But the reading is always os.environ. On a real server, ops sets the variables directly — no .env file needed. Your script reads from the same place either way.
Okay, but when I run python3 analyze.py --service auth --date today, where do those arguments live? That's sys.argv, right?
sys.argv. You're already thinking about it correctly — it's a plain Python list:
import sys
# python3 analyze.py --service auth --date today
print(sys.argv)
# ['analyze.py', '--service', 'auth', '--date', 'today']
script_name = sys.argv[0] # 'analyze.py'
args = sys.argv[1:] # ['--service', 'auth', '--date', 'today']sys.argv[0] is always the script name. Everything after is the raw arguments as strings. It's a list — you can iterate it, index it, slice it.
So sys.argv is the intercom system. The user types something into the console, it travels through sys.argv, and my script picks up the call. I've been refusing to answer the intercom and using input() prompts instead.
input() works for interactive scripts. It breaks the moment you want to schedule the job with cron. A script that waits for keyboard input at 3 a.m. is not a good cron job. And there's one more sys function for production scripts:
import sys, os
if not os.environ.get("LOG_ROOT"):
print("Error: LOG_ROOT not set", file=sys.stderr)
sys.exit(1)sys.exit(1) stops the script and tells the calling process something went wrong. sys.stderr keeps error messages out of your data stream — when you redirect output to a file, only stdout goes to the file. Errors stay visible in the terminal.
sys.stderr is what I've been missing. I pipe the script's output and the error message ends up on line 1 of the CSV. I've been stripping it manually. I could have used file=sys.stderr the whole time.
The mental model: os.environ is for stable configuration that changes by deployment context. sys.argv is for per-invocation arguments. Environment is the thermostat — set once, controls everything. sys.argv is the intercom — you talk into it each time.
And today's problem simulates exactly this: parse KEY=VALUE strings into an env dict, parse argv for script_name and args, read LOG_LEVEL with a default, check for --debug in args. The tricky bit is split("=", 1) to handle values that contain = — like a database URL.
Good catch on the URL case. "DATABASE_URL=postgres://host/db?ssl=true".split("=") gives you four pieces. split("=", 1) gives you exactly two. Tomorrow the ops team needs to move six months of archived logs to cold storage — you're going to meet shutil, Python's moving-company module.
os and sys are the two standard library modules that connect a Python script to its operating environment. They have different scopes: os mediates between Python and the operating system — files, processes, environment variables, working directory. sys mediates between Python and the Python interpreter itself — the module search path, the running version, the current exception, and most importantly sys.argv and sys.exit().
The twelve-factor app methodology codifies what ops engineers have known for decades: configuration that changes between environments — database URLs, log levels, feature flags, API keys — belongs in the environment, not in the code. os.environ exposes the entire process environment as a dict-like object. os.environ.get("KEY", "default") is the canonical read pattern. os.environ["KEY"] = "value" sets a variable for the current process and any child processes it spawns.
The most common mistake is treating the absence of a variable as an error when it should have a sensible default. os.environ.get("LOG_LEVEL", "INFO") is almost always preferable to os.environ["LOG_LEVEL"], which raises KeyError when the variable is not set.
sys.argv is the unprocessed command line as a list of strings. Index 0 is always the script name. Everything after is what the user typed, split on whitespace by the shell. sys.argv is not argument parsing — for that, Week 4 introduces argparse. sys.argv is the raw material. Direct sys.argv access is appropriate for very simple scripts; anything with flags, types, or help text deserves argparse.
Python scripts have three standard streams: sys.stdin (input), sys.stdout (output), sys.stderr (diagnostic). The critical operational distinction: when a user redirects output with python3 script.py > results.csv, only sys.stdout goes to the file. sys.stderr stays on the terminal. Printing errors to sys.stderr with print("Error: ...", file=sys.stderr) ensures they remain visible even when output is piped or redirected.
sys.exit(0) signals success. sys.exit(1) (or any non-zero integer) signals failure. Shell scripts, cron jobs, CI pipelines, and monitoring systems all check exit codes. A script that encounters a fatal error and exits with 0 tells its callers "everything is fine" — which is worse than crashing. sys.exit(1) with an informative message to sys.stderr is the correct pattern for fatal configuration errors.