Look at this script:
for item in items:
if item["value"] > 10: # threshold
send_alert("team@x.com", item) # recipient
if results["fail"] > 5: # max errors
abort()Three numbers / strings buried in the logic. Tomorrow when you want to change the threshold to 20, you scan the code looking for it. Same for adding a second recipient.
The fix: pull them all to the top.
CONFIG = {
"threshold": 10,
"recipient": "team@x.com",
"max_failures": 5,
}
for item in items:
if item["value"] > CONFIG["threshold"]:
send_alert(CONFIG["recipient"], item)
if results["fail"] > CONFIG["max_failures"]:
abort()Same logic. All knobs in one place.
And the behaviour is unchanged?
Identical. The point isn't what the script does — it's where the knobs live. Future-you, edit the dict at the top instead of hunting through 200 lines.
Why a dict instead of just module-level variables?
Both work. A dict has two advantages: (1) easy to dump for logging — log("started", config=CONFIG) records every knob; (2) easy to swap whole — CONFIG = load_config("prod.json") vs "dev.json". Module variables don't compose that way.
For scripts under 100 lines, both shapes are fine. Pick the one your future-self can scan fastest.
Anything you might want to change without touching logic:
Not in CONFIG: anything intrinsic to what the script does. The fact that this script processes emails (not events) is a logic choice, not a config knob.
CONFIG = {
# Inputs
"input_query": "is:unread",
"max_items": 20,
# Behaviour
"threshold": 10,
"max_failures": 5,
"retry_attempts": 3,
# Output
"recipient": "team@x.com",
"alert_subject": "[script] high-value alert",
}Group with comments. Three sections (inputs / behaviour / output) is enough structure for most scripts.
import json
print(f"started: {json.dumps(CONFIG)}")One line at the top of every run records exactly which knobs were in effect. Future-you, comparing today's failed run to yesterday's good one, can diff the config logs and see what changed.
CONFIG = {
"recipient": "team@x.com",
# NOT: "api_key": "sk-...",
}Secrets stay in the environment (os.environ.get("API_KEY")) or in the connection (Composio handles auth). CONFIG is for the things you'd commit to git.
assert CONFIG["threshold"] > 0, "threshold must be positive"
assert CONFIG["recipient"], "recipient is required"
assert CONFIG["max_failures"] >= 0, "max_failures must be non-negative"Fail at script start with a clear message rather than failing 100 lines in with a confusing one. Tomorrow's lesson is the full pre-flight pattern.
# don't
CONFIG = {
"empty_list": [],
"zero": 0,
"tab": "\t",
}If the value never changes — it's a constant of the algorithm — leave it inline. CONFIG is for things that might change between runs/environments. An empty list isn't.
Look at this script:
for item in items:
if item["value"] > 10: # threshold
send_alert("team@x.com", item) # recipient
if results["fail"] > 5: # max errors
abort()Three numbers / strings buried in the logic. Tomorrow when you want to change the threshold to 20, you scan the code looking for it. Same for adding a second recipient.
The fix: pull them all to the top.
CONFIG = {
"threshold": 10,
"recipient": "team@x.com",
"max_failures": 5,
}
for item in items:
if item["value"] > CONFIG["threshold"]:
send_alert(CONFIG["recipient"], item)
if results["fail"] > CONFIG["max_failures"]:
abort()Same logic. All knobs in one place.
And the behaviour is unchanged?
Identical. The point isn't what the script does — it's where the knobs live. Future-you, edit the dict at the top instead of hunting through 200 lines.
Why a dict instead of just module-level variables?
Both work. A dict has two advantages: (1) easy to dump for logging — log("started", config=CONFIG) records every knob; (2) easy to swap whole — CONFIG = load_config("prod.json") vs "dev.json". Module variables don't compose that way.
For scripts under 100 lines, both shapes are fine. Pick the one your future-self can scan fastest.
Anything you might want to change without touching logic:
Not in CONFIG: anything intrinsic to what the script does. The fact that this script processes emails (not events) is a logic choice, not a config knob.
CONFIG = {
# Inputs
"input_query": "is:unread",
"max_items": 20,
# Behaviour
"threshold": 10,
"max_failures": 5,
"retry_attempts": 3,
# Output
"recipient": "team@x.com",
"alert_subject": "[script] high-value alert",
}Group with comments. Three sections (inputs / behaviour / output) is enough structure for most scripts.
import json
print(f"started: {json.dumps(CONFIG)}")One line at the top of every run records exactly which knobs were in effect. Future-you, comparing today's failed run to yesterday's good one, can diff the config logs and see what changed.
CONFIG = {
"recipient": "team@x.com",
# NOT: "api_key": "sk-...",
}Secrets stay in the environment (os.environ.get("API_KEY")) or in the connection (Composio handles auth). CONFIG is for the things you'd commit to git.
assert CONFIG["threshold"] > 0, "threshold must be positive"
assert CONFIG["recipient"], "recipient is required"
assert CONFIG["max_failures"] >= 0, "max_failures must be non-negative"Fail at script start with a clear message rather than failing 100 lines in with a confusing one. Tomorrow's lesson is the full pre-flight pattern.
# don't
CONFIG = {
"empty_list": [],
"zero": 0,
"tab": "\t",
}If the value never changes — it's a constant of the algorithm — leave it inline. CONFIG is for things that might change between runs/environments. An empty list isn't.
Create a free account to get started. Paid plans unlock all tracks.