compute_cpl_per_campaign from Day 11 does exactly one thing: spend / leads. What happens when the ops team exports a CSV and the "spend" column is spelled "spendd"?
compute_cpl_per_campaign crashes the moment it tries c["spend"] — KeyError, because the key doesn't exist. I've seen this in production. The whole script dies on row one.
Right. And the dict doesn't always have the wrong key — sometimes "leads" is there but its value is the string "N/A" because someone typed it manually. Then spend / "N/A" raises a TypeError — or if you run float("N/A") first, a ValueError. Real exports have all three failure modes: missing key, bad value, zero denominator.
So I need three separate if checks before every division? That's going to be longer than the formula itself.
You already know this pattern from your IFERROR formula in Excel — wrap the risky expression, and if anything goes wrong, return a fallback instead of crashing. In Python that's try/except. You write the happy-path code in the try block, and name the exceptions you expect in the except clause:
try:
cpl = c["spend"] / c["leads"]
except (KeyError, ValueError, ZeroDivisionError):
cpl = 0.0One handler, three failure modes. The tuple catches all of them.
The tuple is the key part — I was writing three separate except blocks. And 0.0 as the sentinel makes sense: downstream code can filter cpl == 0.0 to flag broken rows instead of crashing the whole pipeline.
Exactly — a 0.0 in the output is a signal, not an answer. Your data-quality dashboard can count them. Now here's the full safe wrapper — notice the float() call that converts string values before dividing:
def safe_compute_cpl(campaign: dict) -> float:
try:
cpl = float(campaign["spend"]) / float(campaign["leads"])
print(f"CPL: {round(cpl, 2)}")
return round(cpl, 2)
except (KeyError, ValueError, ZeroDivisionError):
print("CPL: 0.0 (bad data)")
return 0.0One function, handles every broken row. And the reason we catch specifically those three — not just except Exception — is that a genuinely unexpected error should still surface and crash loudly, not get swallowed.
That's the rule. except Exception is a silence clause — it hides bugs you haven't thought of yet. Catch only the exceptions you planned for. If something outside that list raises, let it crash: the traceback is your best debugging tool.
try/except wraps risky expressions and substitutes a fallback when a named exception fires.
except (KeyError, ValueError, ZeroDivisionError):
return 0.0Catches all three in one clause — cleaner than three separate blocks.
| Exception | Trigger |
|---|---|
KeyError | Dict key missing — c["spend"] when key is "spendd" |
ValueError | Bad conversion — float("N/A") |
ZeroDivisionError | Zero denominator — spend / 0 |
except ExceptionCatches everything — including bugs you haven't anticipated. Always name the specific exceptions you expect.
compute_cpl_per_campaign from Day 11 does exactly one thing: spend / leads. What happens when the ops team exports a CSV and the "spend" column is spelled "spendd"?
compute_cpl_per_campaign crashes the moment it tries c["spend"] — KeyError, because the key doesn't exist. I've seen this in production. The whole script dies on row one.
Right. And the dict doesn't always have the wrong key — sometimes "leads" is there but its value is the string "N/A" because someone typed it manually. Then spend / "N/A" raises a TypeError — or if you run float("N/A") first, a ValueError. Real exports have all three failure modes: missing key, bad value, zero denominator.
So I need three separate if checks before every division? That's going to be longer than the formula itself.
You already know this pattern from your IFERROR formula in Excel — wrap the risky expression, and if anything goes wrong, return a fallback instead of crashing. In Python that's try/except. You write the happy-path code in the try block, and name the exceptions you expect in the except clause:
try:
cpl = c["spend"] / c["leads"]
except (KeyError, ValueError, ZeroDivisionError):
cpl = 0.0One handler, three failure modes. The tuple catches all of them.
The tuple is the key part — I was writing three separate except blocks. And 0.0 as the sentinel makes sense: downstream code can filter cpl == 0.0 to flag broken rows instead of crashing the whole pipeline.
Exactly — a 0.0 in the output is a signal, not an answer. Your data-quality dashboard can count them. Now here's the full safe wrapper — notice the float() call that converts string values before dividing:
def safe_compute_cpl(campaign: dict) -> float:
try:
cpl = float(campaign["spend"]) / float(campaign["leads"])
print(f"CPL: {round(cpl, 2)}")
return round(cpl, 2)
except (KeyError, ValueError, ZeroDivisionError):
print("CPL: 0.0 (bad data)")
return 0.0One function, handles every broken row. And the reason we catch specifically those three — not just except Exception — is that a genuinely unexpected error should still surface and crash loudly, not get swallowed.
That's the rule. except Exception is a silence clause — it hides bugs you haven't thought of yet. Catch only the exceptions you planned for. If something outside that list raises, let it crash: the traceback is your best debugging tool.
try/except wraps risky expressions and substitutes a fallback when a named exception fires.
except (KeyError, ValueError, ZeroDivisionError):
return 0.0Catches all three in one clause — cleaner than three separate blocks.
| Exception | Trigger |
|---|---|
KeyError | Dict key missing — c["spend"] when key is "spendd" |
ValueError | Bad conversion — float("N/A") |
ZeroDivisionError | Zero denominator — spend / 0 |
except ExceptionCatches everything — including bugs you haven't anticipated. Always name the specific exceptions you expect.
Olivia's weekly campaign CSV sometimes arrives with missing keys (ops team typo'd "spendd" instead of "spend"), non-numeric values ("N/A" in the leads column), or zero-leads rows from paused campaigns. Write `safe_compute_cpl(campaign)` that takes a single campaign dict and returns `spend / leads` rounded to 2 decimal places — or `0.0` if any key is missing, a value is non-numeric, or leads is zero. Use a single `try/except` with a tuple of exception types. For example, `safe_compute_cpl({"spend": 1250.0, "leads": 50})` should return `25.0`.
Tap each step for scaffolded hints.
No blank-editor panic.