A payroll CSV importer runs with PYTHONOPTIMIZE=1 and its `assert` row checks never fire — why, and what should guard those rows instead?
answer
- Check how the interpreter was started
- An environment variable changes compilation
- The checks were removed, not skipped
- Invariants are not input validation
- sys.flags.optimize and __debug__ report it
basics
~20 sPYTHONOPTIMIZE=1 is equivalent to -O, so the compiler emits no bytecode for any assert statement and the row checks were never in the deployed program. Data arriving from a CSV file needs an explicit raise; assertions can only carry internal invariants.
solid answer
~50 s`PYTHONOPTIMIZE=1` sets the optimize level to 1, exactly like `-O`: `__debug__` becomes `False` and every `assert` statement is compiled away, in the importer and in every library it imports. Confirm it from inside the process with `sys.flags.optimize` and `__debug__`, since the flag usually arrives from a base image or a platform default nobody reviewed. Then split the checks by who can break them. A malformed CSV row is the outside world being wrong, so it needs an explicit `if not cond: raise ...` naming the offending row — behaviour that must not depend on how the interpreter was started. An internal cross-check, such as a running total re-added at the end, is a genuine invariant: keep it as an assertion if losing it costs only a diagnostic, and promote it to a real raise if the batch must abort. Finally, log the optimize level at startup so the property stops being invisible.
code
python · 24 linesimport csv
import io
ROWS = 'employee_id,gross_cents\n1001,250000\n1002,-500\n'
def parse_rows(text):
for row in csv.DictReader(io.StringIO(text)):
cents = int(row['gross_cents'])
if cents < 0:
raise ValueError(f"row {row['employee_id']}: negative gross {cents}")
yield row['employee_id'], cents
def total_cents(pairs):
total = sum(cents for _, cents in pairs)
assert total >= 0, 'internal invariant: running total went negative'
return total
try:
print(total_cents(parse_rows(ROWS)))
except ValueError as exc:
print('rejected:', exc)go deeper
Recall the single load-bearing fact: PYTHONOPTIMIZE and -O delete assert statements at compile time, so anything the program must enforce belongs in an if that raises, not in an assertion.
Explain the mechanism end to end — __debug__ is False, no bytecode is emitted, the message is never built — and be able to sort a list of checks into invariants versus contracts with the outside world.
Demonstrate the diagnosis: read sys.flags.optimize inside the process rather than trusting deployment config, trace where the flag came from, convert input checks to explicit raises, and audit asserted expressions for side effects that have silently not run.
Own the decision of whether the platform sets that flag at all, weighing a negligible speed win against a blast radius covering every dependency, and make the interpreter's optimize level a logged, asserted-at-boot property of every service.
### First, confirm what the process is actually running `PYTHONOPTIMIZE=1` is exactly equivalent to starting the interpreter with `-O`. At that level `__debug__` is `False`, and the compiler emits **no bytecode** for any `assert` statement: the condition is not evaluated, the message is not built, nothing is skipped at runtime because nothing was ever generated. Every assertion in the process disappears — the importer's, and every one inside every library it imports. Confirm it from inside the process rather than by reading deployment configuration, which is usually where the surprise came from in the first place. `sys.flags.optimize` returns `0`, `1` or `2`, and `__debug__` is `True` only at level `0`. Bytecode caching will not save you either: an optimized run uses its own `opt-1` cache tag, so bytecode compiled earlier with the assertions still in it is never reused. In practice the flag arrives from somewhere nobody looked at: a base container image that sets it, a platform's default environment, an old deployment manifest copied forward. Nobody edited the importer's source, so nothing in the code review showed it. ### What went wrong in the importer Consider a payroll importer that streams a 2.4 GB batch of CSV rows through several worker threads, each accumulating into a shared running total. Two families of checks were written as assertions: * per-row checks on parsed data — the gross amount is non-negative, the employee id is present, the pay period parses; * one invariant on the shared total — the per-worker sums, re-added at the end, must equal the running total the workers maintained. Under the optimize flag both vanish. The first family was never an assertion's job: a malformed row is the outside world being wrong, not the program having a bug, and it must be rejected with an explicit `raise` of a domain error the caller can catch and report against a row number. The second family genuinely was an invariant — but it was also the only thing standing between an interleaved update on the shared counter and a wrong payroll total posted silently. Losing it did not create the race; it removed the last chance to notice one. That is the distinction to state out loud in an interview: **an assertion documents a belief about your own code; a raise enforces a contract with the outside world.** An assertion may be compiled out without changing what the program guarantees. If removing a check would change what the program guarantees, it was never an assertion. ### The fix, in order 1. **Convert every check on external data into an explicit `if not cond: raise ...`** with a specific exception type and a message naming the offending row. Nothing about that behaviour should depend on how the interpreter was started. 2. **Decide deliberately whether to keep running with the flag at all.** The speed win from `-O` is negligible — it removes assertions, it does not optimize anything else meaningfully — while the blast radius is every dependency in the tree, including libraries that made the same mistake with their own asserts. Dropping the flag is usually the right call. 3. **Make the level observable.** Log `sys.flags.optimize` and `__debug__` at startup alongside the interpreter version, and if the service is one that must run with assertions enabled, check the flag at boot and refuse to start otherwise. A one-line startup check turns an invisible deployment property into an obvious one. 4. **Keep the invariant, but promote the important one.** The cross-check on the shared total is worth running in production; write it as a real conditional that raises and aborts the batch. Keep cheap internal sanity checks as assertions where losing them under `-O` costs only a diagnostic. 5. **Audit for side effects inside assertions.** Any asserted expression that mutates state, consumes from an iterator or advances a cursor has been silently skipped for as long as the flag has been set, and that produces missing work rather than a failed check. ### The traps to avoid while fixing it Wrapping the assertion in `try: ... except AssertionError:` does not rescue it — a compiled-out statement raises nothing, so the handler never runs, and the code now looks defended. Catching `AssertionError` at an API boundary and turning it into a client-facing validation error is the same mistake in reverse: `AssertionError` means "this program has a bug", and it is caught by any broad `except Exception:` handler, so invariant failures end up logged as ordinary errors and lost in the noise. And "we'll just never set the flag again" is a promise about an environment, not a property of the code; the code has to be correct regardless of how it is launched.
- Would wrapping the row check in `try: ... except AssertionError:` have made it reliable?No, and it makes things worse. A compiled-out assertion raises nothing, so the handler never runs while the code now looks defended. The deeper problem is what `AssertionError` means: it says the program has a bug, not that the input was invalid, and it is caught by any broad `except Exception:` handler, so real invariant failures get logged as ordinary errors and disappear into the noise.
- Is there ever a good reason to deploy a Python service with `-O` or PYTHONOPTIMIZE set?Rarely. It removes assertions and does essentially nothing else for speed, so the win is negligible while the blast radius covers every dependency — including libraries that wrongly used assertions for validation. The defensible case is a measured hot loop whose internal invariant checks are genuinely expensive, and even then the honest fix is usually to guard that one check behind a flag your own code owns.
- What else silently changes in a codebase that has been running with assertions compiled out for months?Any side effect written inside an asserted expression has not happened. A line that both advances an iterator and checks the value, or that calls a function for its return code, has been skipped for the entire period, and the symptom is missing work rather than a failed check. Audit for asserted expressions that are not pure before you flip the flag back.
saying these in an interview costs you the question
- Wraps the assertion in try/except and calls it validated
- Says PYTHONOPTIMIZE only makes the interpreter faster
- Keeps using assert for CSV or request-payload validation
- Believes a prebuilt .pyc preserves the assertions
- Cannot name a runtime way to read the optimize level
- Leaves state-changing calls inside asserted expressions