A script that runs against the same endpoint and same credentials in dev and in production is a script that will eventually email your real users from your dev box. Separate the two.
import os
CONFIGS = {
"dev": {
"api_url": "https://api-staging.example.com",
"sender": "dev-test@example.com",
},
"prod": {
"api_url": "https://api.example.com",
"sender": "alerts@example.com",
},
}
env = os.environ.get("ENV", "dev")
config = CONFIGS[env]
print(f"selected config: {env}")Why default to dev?
Fail safe. A missing or misspelled ENV should land you in dev — emails go to your test address, no real customer is touched. Defaulting to prod would be the opposite: misconfigured = production blast.
For very sensitive operations (payment captures, data deletion), some teams go further: refuse to run when ENV isn't set explicitly. os.environ["ENV"] (raise on missing) is the right reflex there.
And what's typically in a config block?
API URL. Sender address (so dev emails come from a different address). Database connection string. Feature flags (enable_new_dispatcher: false in prod for now). Logging verbosity (debug in dev, info in prod). Anything that's environment-specific, not secret-specific.
A script with one set of credentials and one set of endpoints will eventually do prod work from dev:
The fix is configuration that flips between dev and prod based on a single env var.
import os
CONFIGS = {
"dev": {
"api_url": "https://api-staging.example.com",
"sender": "dev-test@example.com",
"sheet_name": "Sheet1-dev",
"log_level": "DEBUG",
},
"prod": {
"api_url": "https://api.example.com",
"sender": "alerts@example.com",
"sheet_name": "Sheet1",
"log_level": "INFO",
},
}
env = os.environ.get("ENV", "dev")
if env not in CONFIGS:
raise RuntimeError(f"unknown ENV: {env}")
config = CONFIGS[env]Belongs in CONFIGS dict | Belongs in env vars |
|---|---|
| URLs (no credentials) | API keys, tokens |
| Sender addresses | Database passwords |
| Feature flags | Webhook signing secrets |
| Sheet/document names | Anything sensitive |
| Log levels | (rule of thumb: if it would be a problem to commit to git, it's a secret) |
The rule: secrets in env vars (different value per environment, set by deployment); non-secret config in code (same dict checked in, key chosen by ENV).
env = os.environ.get("ENV", "dev") # safe defaultvs.
env = os.environ.get("ENV", "prod") # WRONG — misconfigured = productionIf the env var is missing or misspelled, dev is the safer landing spot.
For irreversible operations (sending real email, charging cards), add an extra check:
if env == "prod":
confirm = os.environ.get("CONFIRM_PROD")
if confirm != "yes":
raise RuntimeError("prod operation requires CONFIRM_PROD=yes")Forces a deliberate decision before the script touches production state.
Larger systems have more: dev / staging / prod-eu / prod-us. Same pattern, more keys in the dict. Some teams use a single ENV=staging with config layered as defaults → staging → prod-eu overrides — the dict-of-dicts grows into a small layered config system. For most scripts, a flat CONFIGS[env] is enough.
A script that runs against the same endpoint and same credentials in dev and in production is a script that will eventually email your real users from your dev box. Separate the two.
import os
CONFIGS = {
"dev": {
"api_url": "https://api-staging.example.com",
"sender": "dev-test@example.com",
},
"prod": {
"api_url": "https://api.example.com",
"sender": "alerts@example.com",
},
}
env = os.environ.get("ENV", "dev")
config = CONFIGS[env]
print(f"selected config: {env}")Why default to dev?
Fail safe. A missing or misspelled ENV should land you in dev — emails go to your test address, no real customer is touched. Defaulting to prod would be the opposite: misconfigured = production blast.
For very sensitive operations (payment captures, data deletion), some teams go further: refuse to run when ENV isn't set explicitly. os.environ["ENV"] (raise on missing) is the right reflex there.
And what's typically in a config block?
API URL. Sender address (so dev emails come from a different address). Database connection string. Feature flags (enable_new_dispatcher: false in prod for now). Logging verbosity (debug in dev, info in prod). Anything that's environment-specific, not secret-specific.
A script with one set of credentials and one set of endpoints will eventually do prod work from dev:
The fix is configuration that flips between dev and prod based on a single env var.
import os
CONFIGS = {
"dev": {
"api_url": "https://api-staging.example.com",
"sender": "dev-test@example.com",
"sheet_name": "Sheet1-dev",
"log_level": "DEBUG",
},
"prod": {
"api_url": "https://api.example.com",
"sender": "alerts@example.com",
"sheet_name": "Sheet1",
"log_level": "INFO",
},
}
env = os.environ.get("ENV", "dev")
if env not in CONFIGS:
raise RuntimeError(f"unknown ENV: {env}")
config = CONFIGS[env]Belongs in CONFIGS dict | Belongs in env vars |
|---|---|
| URLs (no credentials) | API keys, tokens |
| Sender addresses | Database passwords |
| Feature flags | Webhook signing secrets |
| Sheet/document names | Anything sensitive |
| Log levels | (rule of thumb: if it would be a problem to commit to git, it's a secret) |
The rule: secrets in env vars (different value per environment, set by deployment); non-secret config in code (same dict checked in, key chosen by ENV).
env = os.environ.get("ENV", "dev") # safe defaultvs.
env = os.environ.get("ENV", "prod") # WRONG — misconfigured = productionIf the env var is missing or misspelled, dev is the safer landing spot.
For irreversible operations (sending real email, charging cards), add an extra check:
if env == "prod":
confirm = os.environ.get("CONFIRM_PROD")
if confirm != "yes":
raise RuntimeError("prod operation requires CONFIRM_PROD=yes")Forces a deliberate decision before the script touches production state.
Larger systems have more: dev / staging / prod-eu / prod-us. Same pattern, more keys in the dict. Some teams use a single ENV=staging with config layered as defaults → staging → prod-eu overrides — the dict-of-dicts grows into a small layered config system. For most scripts, a flat CONFIGS[env] is enough.
Create a free account to get started. Paid plans unlock all tracks.