← Course Home
Module 9

Essential Libraries

Reading environment variables, talking to APIs, parsing JSON, reading CLI arguments, and working with timestamps — the toolbox every IT/SecOps script actually reaches for.

Concept

Reading Env Vars Safely

Environment variables are settings passed to your script from outside — API tokens, config paths — instead of hardcoded into the file. os.environ.get() reads one safely, returning a default instead of crashing if it's missing.

import os

token = os.environ.get("OKTA_TOKEN", "not set")
print(token)
Lab 1

Run the example above. What prints (you haven't set this variable)?

not set
Lab 2

Try os.environ["OKTA_TOKEN"] (direct access, no default) on a variable you haven't set. What happens?

CrashesKeyError: 'OKTA_TOKEN'
Lab 3

Read the PATH variable (which does exist on your system) with os.environ.get("PATH"). Print just its type.

<class 'str'>
Lab 4

Why is .get() with a default almost always better than direct [] access for env vars? One sentence.

a missing/unset var shouldn't crash your whole script
Lab 5

Write a function get_token() that reads MY_API_TOKEN safely, returning None if unset. Call it, print the result.

None
Now go practice

Open problems/01_env_vars.py and do all 5 problems locally.

Concept

What a Request/Response Is

A request is your script asking a server for something over the network. A response is what the server sends back — a status code (did it work?) plus a body (usually JSON) with the actual data.

This sub-skill cluster (2–5) is syntax and concept only — no local graded problems, since a real network call can't be reliably tested offline. Read the syntax, run the labs if you have real credentials to test with, and recognize the shape when you see it in an interview or on the job.

Lab 1

In one sentence: what's the difference between a request and a response?

request = what you send; response = what comes back
Lab 2

Name one piece of information a response always includes, regardless of what API you're calling.

a status code
Lab 3

Why does almost every modern API return JSON instead of plain text?

JSON has structure — keys and nested data — plain text doesn't
Lab 4

What do you think happens if the network is down when you send a request? (predict, don't run)

the request fails/times out — no response at all
Lab 5

Name one real IT/SecOps task that requires talking to an API (Okta, Slack, or Google).

e.g. listing all Okta users, or posting a Slack alert
Concept

requests.get() / post() Syntax

The requests library sends HTTP requests. get() asks for data. post() sends data. Both return a Response object.

import requests

response = requests.get(
    "https://example.okta.com/api/v1/users",
    headers={"Authorization": f"SSWS {token}"}
)

response = requests.post(
    "https://hooks.slack.com/services/...",
    json={"text": "Alert: something happened"}
)
Lab 1

In the get() example, what argument carries the auth token?

headers
Lab 2

In the post() example, what argument carries the message body?

json
Lab 3

Which verb would you use to fetch a list of users — get or post?

get
Lab 4

Which verb would you use to send a Slack alert message — get or post?

post
Lab 5

Write the syntax (don't run it) for a get() call to "https://api.example.com/status" with no headers.

requests.get("https://api.example.com/status")
Concept

status_code

Every response has a status_code — a number telling you if the request worked. 200 means success. 4xx means your request was wrong (bad auth, bad URL). 5xx means the server itself failed.

if response.status_code == 200:
    print("success")
elif response.status_code == 401:
    print("bad auth token")
elif response.status_code == 404:
    print("not found")
else:
    print(f"unexpected: {response.status_code}")
Lab 1

What does status code 200 mean?

success
Lab 2

What does status code 401 usually mean?

unauthorized — bad or missing auth
Lab 3

What does status code 404 mean?

not found
Lab 4

Is a 500 error your script's fault, or the server's fault?

the server's
Lab 5

Write the if/elif/else branching above from memory (don't look back).

Concept

Checkpoint: .json() and a Full Fetch Function

response.json() converts the response body into a normal Python list/dict — the same shapes you've used all course. Combine everything: request, status check, and JSON conversion, into one function.

def fetch_users(base_url, token):
    response = requests.get(
        f"{base_url}/api/v1/users",
        headers={"Authorization": f"SSWS {token}"}
    )
    if response.status_code != 200:
        raise Exception(f"fetch failed: {response.status_code}")
    return response.json()
Lab 1

What Python type does .json() usually return for a "list users" endpoint?

a list (of dicts)
Lab 2

Why does fetch_users check status_code BEFORE calling .json()?

an error response might not have valid JSON to parse
Lab 3

What does fetch_users do instead of returning when the status code is bad?

it raises an exception
Lab 4

Sketch (in words) how you'd call this in a chain: fetch_users → what would the next function do with the returned list?

e.g. filter/count it, same as every fixture-based chain so far
Lab 5 · Checkpoint

Write fetch_users exactly as shown from memory, plus a fake call_it() that would call it with a real base_url and token (don't actually run the network call).

Concept

json.loads()

APIs send JSON as raw text. json.loads() converts that text into real Python lists/dicts you can work with. (Loading JSON from a file uses json.load() instead — you'll meet that in Module 10.)

import json

raw = '{"id": "00u001", "status": "ACTIVE"}'
user = json.loads(raw)
print(user["status"])
print(type(user))
Lab 1

Run the example above. Confirm both printed lines.

ACTIVE then <class 'dict'>
Lab 2

Parse '[1, 2, 3]' with json.loads(). Print its type.

<class 'list'>
Lab 3

Parse '{"groups": ["IT", "VPN"]}', then print just the second group name.

VPN
Lab 4

Try json.loads('not valid json'). What happens?

Crashesjson.JSONDecodeError
Lab 5

Parse '{"active": true, "count": null}'. Print both values and their Python types.

True <class 'bool'> then None <class 'NoneType'>
Now go practice

Open problems/06_json_loads.py and do all 5 problems locally.

Concept

json.dumps()

The reverse of loads()json.dumps() turns a Python list/dict back into JSON text, for sending in a request body or writing to a file.

import json

alert = {"text": "Something happened", "level": "warning"}
text = json.dumps(alert)
print(text)
print(type(text))
Lab 1

Run the example above. Confirm both printed lines.

{"text": "Something happened", "level": "warning"} then <class 'str'>
Lab 2

json.dumps([1, 2, 3]) — print the result and its type.

[1, 2, 3] then <class 'str'>
Lab 3

Round-trip: take a dict, dumps() it, then loads() the result back. Confirm it equals the original with ==.

True
Lab 4

Try json.dumps on a dict containing a datetime object (not a plain value). What happens?

CrashesTypeError: Object of type datetime is not JSON serializable
Lab 5

Add indent=2 to json.dumps() on a small dict. What changes about the output?

it prints multi-line and readable, instead of one line
Now go practice

Open problems/07_json_dumps.py and do all 5 problems locally.

Concept

sys.argv

sys.argv is a list of the words typed on the command line when your script ran. sys.argv[0] is always the script's own filename; the arguments start at index 1.

# saved as report.py, run as: python report.py IT 2026-07-01
import sys

print(sys.argv)
print(sys.argv[1])
Lab 1

Save the example as a file and run it with an argument. What does sys.argv[0] hold?

the script's own filename
Lab 2

Run your script with two arguments. What's len(sys.argv)?

3 (filename + 2 args)
Lab 3

Run your script with ZERO arguments. Try to access sys.argv[1]. What happens?

CrashesIndexError
Lab 4

Why is it risky to access sys.argv[1] without checking len(sys.argv) first?

the script crashes if the user forgets to pass an argument
Lab 5

Write a guarded version that prints sys.argv[1] if present, else prints "no argument given".

Now go practice

Open problems/08_sys_argv.py and do all 5 problems locally.

Concept

argparse

argparse is a library that turns raw sys.argv words into named, typed, validated arguments — with free help text and error messages, instead of you hand-checking positions.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--department", help="filter by department")
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args()

print(args.department)
print(args.verbose)
sys.argvargparse
Accessby position ([1], [2])by name (args.department)
Missing argIndexError, you guard it yourselfbuilt-in default / required check
Bad usageno help textauto-generated --help
Lab 1

Save the example, run it with --department IT --verbose. What prints?

IT then True
Lab 2

Run it with NO arguments. Does it crash?

No — None then False, since neither is required
Lab 3

Add required=True to the --department argument. Run with no arguments -- what happens?

argparse prints a usage error and exits — no Python traceback
Lab 4

Run your script with --help. What do you see?

auto-generated usage/help text, built for free
Lab 5

In one sentence: what's the main advantage of argparse over hand-parsing sys.argv?

named, validated arguments with built-in help and error handling
Now go practice

Open problems/09_argparse.py and do all 4 problems locally.

Concept

Checkpoint: A Mini CLI Tool

Combine env vars, JSON, and argparse into one small tool — the shape of a real script you'd run from a terminal.

import os, json, argparse

def build_report(department, users):
    matches = [u for u in users if u["profile"].get("department") == department]
    return json.dumps({"department": department, "count": len(matches)})

parser = argparse.ArgumentParser()
parser.add_argument("--department", required=True)
args = parser.parse_args()

token = os.environ.get("MY_API_TOKEN", "no-token")
print(build_report(args.department, USERS))
Lab 1

Write build_report exactly as shown. Call it directly (not through argparse) with "Engineering" and the real USERS fixture. Print the result.

{"department": "Engineering", "count": 3}
Lab 2

Call build_report with a department that doesn't exist, like "Marketing". What prints?

{"department": "Marketing", "count": 0}
Lab 3

Parse the JSON string from Lab 1 back into a dict with json.loads(). Print just the count.

3
Lab 4

What does os.environ.get("MY_API_TOKEN", "no-token") print if you haven't set that variable?

no-token
Lab 5 · Checkpoint

Write the full script (env var read + build_report + a fake argparse-style call using a hardcoded department instead of real CLI input) and run it end-to-end.

Now go practice

Open problems/10_mini_cli_tool.py — 5 problems, #5 is a checkpoint combining sub-skills 1, 6-9.

Concept

datetime.fromisoformat()

APIs send timestamps as text, like "2026-07-24T09:14:00Z". datetime.fromisoformat() parses that text into a real datetime object you can compare and do math with.

from datetime import datetime

text = "2026-07-24T09:14:00Z"
when = datetime.fromisoformat(text)
print(when)
print(type(when))
print(when.year)
Lab 1

Run the example above. Confirm all three printed lines.

2026-07-24 09:14:00+00:00, <class 'datetime.datetime'>, 2026
Lab 2

Parse USERS[0]["lastLogin"] (Julio). Print just .month and .day.

7 then 24
Lab 3

Parse two different users' lastLogin values and compare them with <. Which one is earlier?

depends on the two chosen -- confirm the earlier date compares as smaller
Lab 4

Try parsing USERS[3]["lastLogin"] (Dana Morris) directly. What happens?

CrashesTypeError, it's None, not a string
Lab 5

Write a loop that parses every user's lastLogin EXCEPT Dana's (skip None with an if), printing each parsed year.

11 lines, all 2025 or 2026
Now go practice

Open problems/11_fromisoformat.py and do all 5 problems locally.

Concept

timedelta and datetime.now()

Subtracting two datetime objects gives a timedelta — a span of time. datetime.now() gives you the current moment to compare against.

from datetime import datetime

then = datetime.fromisoformat("2026-06-01T00:00:00")
now = datetime.fromisoformat("2026-07-26T00:00:00")
span = now - then
print(span)
print(type(span))
print(span.days)
Lab 1

Run the example above. Confirm all three printed lines.

55 days, 0:00:00, <class 'datetime.timedelta'>, 55
Lab 2

Print type(datetime.now()).

<class 'datetime.datetime'>
Lab 3

Compute the span between Julio's lastLogin and Tri Nguyen's lastLogin (both parsed). Print .days.

a positive number of days
Lab 4

Subtract a LATER date minus an EARLIER date, then the reverse (earlier minus later). What's different about the sign?

the reversed subtraction gives a negative .days
Lab 5

Write a function days_since(dt) that returns (reference_now - dt).days, using a fixed reference datetime you define, not the real datetime.now() (so it's reproducible). Test it on Julio's parsed lastLogin.

Now go practice

Open problems/12_timedelta.py and do all 5 problems locally.

Concept

Guarding None Timestamps

Real records have missing timestamps — a user who's never logged in. Guard before parsing, the same way you've guarded missing dict keys all course.

def parse_login(user):
    raw = user.get("lastLogin")
    if raw is None:
        return None
    return datetime.fromisoformat(raw)
Lab 1

Write parse_login exactly as shown. Call it on USERS[3] (Dana). Print the result.

None
Lab 2

Call it on USERS[0] (Julio). Print the result and its type.

→ a datetime, type <class 'datetime.datetime'>
Lab 3

Loop all 12 USERS, calling parse_login on each -- confirm no crash, and exactly one None.

→ 1 None, 11 real datetimes
Lab 4

Write a function that prints "never logged in" for a None result, or the date otherwise. Run it over all 12 users.

→ 11 dates, 1 "never logged in"
Lab 5

Why is guarding here the same shape as guarding .get("department", "Unknown") from Module 8? One sentence.

both check for missing/None data before using it, instead of assuming it's always present
Now go practice

Open problems/13_guard_none_timestamps.py and do all 5 problems locally.

Concept

Full-Fixture Timestamp Report

Combine parsing, guarding, and comparison into one report over the real fixture.

Lab 1

Using a fixed reference datetime of 2026-07-26, compute days-since-login for every user with a real lastLogin (skip Dana). Print each as "name: N days".

11 lines
Lab 2

Count how many users logged in more than 90 days ago.

4 (Bina, Tri, Rosa, Pat)
Lab 3

Find the single most-recently-logged-in user (smallest days-since). Print their name.

Amy or Sam (both 0 days -- print whichever your loop finds first)
Lab 4

Build a JSON string (with json.dumps) summarizing: total users, users never logged in, users inactive 90+ days.

{"total": 12, "never_logged_in": 1, "inactive_90_plus": 4}
Now go practice

Open problems/14_timestamp_report.py and do all 4 problems locally.

Concept

Final Checkpoint: Everything but requests

One script combining env vars, JSON, datetime, and argparse-style argument handling (hardcoded stand-in for real CLI input) over the full fixture.

Lab 1

Read a (probably-unset) env var for a report title, defaulting to "User Report".

User Report
Lab 2

Build the inactivity summary dict from sub-skill 14's Lab 4.

Lab 3

Convert it to a JSON string with json.dumps(..., indent=2). Print it.

multi-line, readable JSON
Lab 4

Wrap the whole thing in a function build_report(title) and call it with your env-var title from Lab 1.

Lab 5 · Final checkpoint

One script: env var title, guarded timestamp parsing over all 12 users, the inactivity summary, and a pretty-printed JSON report -- zero crashes.

This is the big one -- take your time.
Now go practice

Open problems/15_final_checkpoint.py — 5 problems, #5 is the final checkpoint for all of Module 9.

Complete

Module 9 done

os.environ → requests/response shape (recognition only) → json.loads/dumps → sys.argv → argparse → mini CLI tool → datetime.fromisoformat → timedelta → guarding None timestamps → full-fixture timestamp report.

Run python check.py in problems/ to grade everything.