Interacting with the World
Expert · 18 min read · ▶ live playground · ✦ checkpoint
Local automation cleans folders you control. World-facing automation touches systems that can surprise other people: commands, schedulers, email, chat bots, APIs, browsers, operating-system actions, and files dropped by another app.
The skill is not "make Python click everything." The skill is building a small, boring safety cage around every outside action: dry-run first, clear options, bounded retries, audit records, hidden secrets, and human confirmation when the action can cause damage.
Treat the outside world as a boundary
An external system is anything your script does not fully control: another command, an email server, a webhook endpoint, a browser window, a shared folder, or a scheduled runner. Crossing that boundary changes the stakes. A bug can send duplicate messages, hit a rate limit, overwrite another tool's output, or wake someone at 2 a.m.
The safest shape is the same one you built in the CLI lesson:
- Parse explicit options.
- Build an action plan.
- Print the plan by default.
- Require
--applyplus confirmation for risky work. - Return a clear exit code.
Here is a live example that watches a folder of files "dropped" by another system. It does not call an API or send email. It creates sample files, reads them, and builds dry-run action records.
from pathlib import Path
import json
drop_dir = Path("world_sandbox") / "drops"
drop_dir.mkdir(parents=True, exist_ok=True)
sample_invoices = [
{"id": "INV-1001", "customer": "Mina", "amount": 125},
{"id": "INV-1002", "customer": "Ravi", "amount": 45},
]
for invoice in sample_invoices:
path = drop_dir / (invoice["id"] + ".json")
path.write_text(json.dumps(invoice), encoding="utf-8")
def build_actions(folder):
actions = []
for path in sorted(folder.glob("*.json")):
invoice = json.loads(path.read_text(encoding="utf-8"))
base_key = "invoice:" + invoice["id"]
actions.append({
"kind": "webhook",
"target": "accounting-api",
"idempotency_key": base_key + ":webhook",
"payload": {
"invoice_id": invoice["id"],
"amount": invoice["amount"],
},
})
if invoice["amount"] >= 100:
actions.append({
"kind": "email",
"target": "finance-review",
"idempotency_key": base_key + ":email",
"subject": "Review " + invoice["id"],
})
return actions
for action in build_actions(drop_dir):
print(
"DRY RUN",
action["kind"],
"->",
action["target"],
"[" + action["idempotency_key"] + "]",
)DRY RUN webhook -> accounting-api [invoice:INV-1001:webhook]DRY RUN email -> finance-review [invoice:INV-1001:email]DRY RUN webhook -> accounting-api [invoice:INV-1002:webhook]
An idempotency key is a stable label for one intended outside action. If the same invoice is processed twice, the key stays the same, so the receiving system or your own log can recognize "I have seen this action before." Idempotence means rerunning does not create a second surprise.
Know which actions stay static
Some automation families are important, but they are not safe to run inside this lesson:
Static shapes only - not run in this browser lesson:
# Tool execution with subprocess
python resize_images.py --input drops --output thumbnails --dry-run
# Scheduler / cron shape
0 8 * * 1-5 /path/to/.venv/bin/python /app/check_invoices.py --dry-run
# Notification shape
python notify_team.py --channel finance-alerts --dry-run
# API / webhook shape
python sync_crm.py --endpoint https://api.example.invalid/leads --dry-run
# Browser / OS automation shape
python browser_audit.py --site internal-dashboard --dry-runSubprocess automation means using Python to run another command-line tool. A scheduler, such as cron, runs a command at a chosen time. A webhook is an HTTP endpoint meant to receive event notifications. Browser automation drives a browser, and OS automation changes local machine state such as windows, files, or settings.
Those tools are powerful because they leave Python's sandbox. That is exactly why a lesson should show their shape without firing them.
Retry carefully, not forever
Outside systems fail in ordinary ways: a temporary 503, a rate-limited 429, a timeout, or a network connection that drops. A retry is trying again after a failure. A rate limit is a service saying "slow down." Retrying forever is not resilience; it is a bug with a louder drumbeat.
Use bounded retries: decide which failures are retryable, cap the number of attempts, and respect Retry-After when a service gives you one.
responses = [
{"status": 429, "retry_after": 60},
{"status": 503, "retry_after": None},
{"status": 200, "retry_after": None},
]
def retry_decision(response, attempt, max_attempts):
status = response["status"]
if 200 <= status < 300:
return "success", 0
if attempt >= max_attempts:
return "give up", 0
if status == 429 and response["retry_after"] is not None:
return "retry after rate limit", response["retry_after"]
if status in (500, 502, 503, 504):
return "retry temporary failure", 10 * (2 ** (attempt - 1))
return "do not retry", 0
for attempt, response in enumerate(responses, start=1):
decision, wait = retry_decision(response, attempt, max_attempts=3)
print(
"attempt",
str(attempt) + ":",
"HTTP",
response["status"],
"->",
decision + ",",
"wait",
str(wait) + "s",
)
if decision in ("success", "give up", "do not retry"):
breakattempt 1: HTTP 429 -> retry after rate limit, wait 60sattempt 2: HTTP 503 -> retry temporary failure, wait 20sattempt 3: HTTP 200 -> success, wait 0s
The exact delays are a policy choice. The important part is that the policy exists in code, can be reviewed, and stops.
Try it - build a world-facing command safely
This playground simulates a command that would notify another system when a backup is ready. It never sends anything. It builds payload dictionaries, audit records, and exit codes from explicit options.
Read the three runs as a safety story. The default is dry run. --apply alone is rejected with exit code 2, because a risky action needs explicit confirmation. --apply --confirm builds the approved action records, but this browser example still does not send them anywhere.
An audit trail is a record of what the automation decided and why. In real tools, those records usually go to logs, a database table, or a monitoring system. Do not log secrets. Log the secret's name, presence, or source instead.
Configuration, secrets, and people
Configuration is the values that change between environments: endpoint names, channel names, folder paths, retry limits, and dry-run defaults. A secret is a sensitive value such as an API token, bot token, email password, or signing key. Your script can check that a secret is configured, but it should not print the secret itself.
Human confirmation belongs around actions that are hard to undo: sending many messages, closing issues, deleting files, pushing records into another system, or driving a browser through a production admin page. The right question is not "Can Python do it?" Python can. The better question is "What prevents one wrong option from doing it twice?"
A world-facing automation checklist:
- Safe default: dry run unless
--applyis explicit. - Confirmation: require a second signal for risky work.
- Idempotency: stable keys for repeated events.
- Bounded retries: limited attempts, rate-limit respect.
- Audit trail: action records without secrets.
- Clear streams: stdout for normal results, stderr for problems.
- Exit codes:
0for success, nonzero for refusal or failure.
Section 24 started with local folder automation, wrapped it into CLIs, and ended at the boundary of external systems. The same discipline carries forward: when your next Python program analyzes data, trains a model, calls an AI API, or ships as a capstone, make every outside action explicit, repeatable, and reviewable before it is powerful.
Checkpoint
Answer all three to mark this lesson complete