What does Python's `assert` statement do, and what happens to it under `python -O`?
answer
- A statement, not a function call
- The interpreter can remove it entirely
- A command-line flag changes the outcome
- __debug__ is False under -O
- Falsy condition raises AssertionError
basics
~20 sassert expr, msg raises AssertionError with msg when expr is falsy and does nothing otherwise. Starting the interpreter with -O, or with PYTHONOPTIMIZE set, removes every assert statement at compile time, so the check never runs.
solid answer
~40 s`assert` is a statement, not a function. The compiler expands `assert expr, msg` into `if __debug__: if not expr: raise AssertionError(msg)`. `__debug__` is a compile-time constant that is `True` for a normal run and `False` when the interpreter starts with `-O` or with `PYTHONOPTIMIZE` set to a non-empty value; in that mode the compiler emits **no bytecode at all** for the statement, so the condition is never evaluated and any side effect inside it disappears too. `sys.flags.optimize` reports the level (0, 1 for `-O`, 2 for `-OO`). Because that flag is a property of the deployment and not of your source, `assert` can only ever be a developer-facing invariant check. Anything the program depends on — argument validation, parsing untrusted data, permission checks — has to be an explicit `if not cond: raise ...`.
code
python · 15 linesimport sys
def check(count):
assert count >= 0, f'negative count: {count}'
return count
print('optimize level:', sys.flags.optimize, '__debug__:', __debug__)
try:
check(-1)
except AssertionError as exc:
print('raised:', exc)
else:
print('assert did not run')go deeper
Be ready to state the two operands, the exception type, and the one-line consequence: a flag on the command line can delete the statement, so never let a program's correctness depend on an assertion.
Explain the expansion into if __debug__: if not expr: raise AssertionError(msg), that the message is evaluated only on failure, and that -O removes the bytecode rather than skipping a runtime check.
Show the operational angle: the optimize level arrives from a base image or an environment variable nobody reviewed, it silences every library's assertions as well as yours, and sys.flags.optimize logged at boot is what makes that visible.
Own the policy. Decide whether the codebase ships with -O at all, given the negligible speed win against a blast radius covering every dependency, and write down the house rule that separates invariants from enforced contracts.
### `assert` is a statement, not a function `assert` belongs to Python's grammar in the same family as `raise`, `return` and `del`. It takes one or two comma-separated expressions: the condition, and an optional failure message. It has no argument list of its own, which is why writing it as if it were a call is a real bug rather than a style nit. The compiler expands `assert expr, msg` into roughly this: ```python if __debug__: if not expr: raise AssertionError(msg) ``` Three things follow straight from that expansion. The condition is put through the ordinary truthiness test, so any object works, not just a `bool`. The message operand is evaluated *only* when the condition is falsy, so an expensive f-string message costs nothing on the happy path. And every part of it sits under `__debug__`. ### `__debug__` is a compile-time constant `__debug__` is a builtin name the compiler treats specially. You cannot rebind it — `__debug__ = False` is a `SyntaxError: cannot assign to __debug__` — and its value is fixed when the interpreter starts. In a normal run it is `True`. Start the interpreter with `-O`, or with the environment variable `PYTHONOPTIMIZE` set to a non-empty value, and it is `False`. Because the compiler knows the constant statically, it then emits **no bytecode at all** for the assert statement: the condition is never evaluated, the message is never built, and no branch is taken at runtime. This is removal at compile time, not a runtime check that quietly returns. `sys.flags.optimize` reports the level a process is running at: `0` for a normal run, `1` for `-O`, `2` for `-OO`. Both `__debug__` and that flag are readable from inside the program, which is the only reliable way to find out what a deployed process actually did. Bytecode caching follows the same split: an optimized run reads and writes a separately tagged cache file, carrying an `opt-1` tag alongside the ordinary one, so an interpreter started with `-O` never picks up cached bytecode that still contains the assertions. ### Why this makes `assert` a developer-only tool The optimize level is a property of the *deployment*, not of the source. A base container image, a platform default, a CI job or an operator's habit can set `PYTHONOPTIMIZE` without anyone touching your code, and every assertion in the process — yours and every library's — disappears together. So the honest rule is: * **`assert` states something that is true unless your own code has a bug.** Internal invariants, post-conditions, "this cache and this recomputed value must agree", "this branch is unreachable". Losing the check under `-O` loses a diagnostic, not a guarantee. * **`raise` states something that may be false because the outside world is not what you hoped.** Argument validation on a public function, parsed input from a file or a request, permission checks, configuration sanity. Those must be `if not cond: raise SomeError(...)` with a domain-appropriate exception type. The failure mode of getting this backwards is nasty precisely because it is silent: the code behaves identically in development, and in production the check is simply not there. ### Side effects inside an assertion Because the whole statement can vanish, anything with a side effect inside the asserted expression vanishes with it. A line that both consumes an item and checks it will consume nothing under `-O`, and the bug shows up as missing work rather than as a failed assertion. Keep asserted expressions pure and cheap. ### `AssertionError` in an exception hierarchy `AssertionError` derives from `Exception`, so a broad `except Exception:` handler catches it exactly like anything else. That is how a service ends up swallowing its own invariant failures and logging them as a generic error. It is also why `AssertionError` is a poor exception type to expose at an API boundary: to a reader it says "this program has a bug", not "your input was invalid", and a caller has no way to distinguish the two. ### What interviewers are actually testing The question is a proxy for a design instinct. A candidate who says "`assert` raises `AssertionError` when the expression is falsy" has answered half of it. The other half — that a command-line flag or an environment variable can delete the statement before it ever runs, and that this is what disqualifies `assert` from doing any job the program depends on — is the part that separates someone who has only written asserts in a scratch script from someone who has reasoned about what runs in production.
- Is the message expression in `assert expr, msg` evaluated when the assertion passes?No. The compiler puts the message construction inside the failure branch, so it is built only when the condition is falsy. An expensive f-string or a `repr()` of a large object in the message costs nothing on the happy path, which is why detailed assertion messages are cheap. The condition itself, of course, is evaluated every time — unless the interpreter was started with `-O`, in which case neither one is.
- How can a running process tell whether it was started with assertions enabled?Read `sys.flags.optimize`, which is `0` for a normal run, `1` for `-O` and `2` for `-OO`, or read the `__debug__` constant, which is `True` only at level 0. Logging one of them at startup alongside the interpreter version turns an invisible deployment property into an obvious one. A service that genuinely relies on its assertions can check the flag at boot and refuse to start.
- If a module was already compiled to bytecode without `-O`, does running with `-O` reuse that cached file and keep the assertions?No. An optimized run uses a separate cache tag, so an `opt-1`-tagged file sits next to the ordinary one and the interpreter only loads bytecode matching its own optimize level. You never get a mixed state where a stale cache smuggles assertions into an optimized process, and clearing the cache changes nothing about the behaviour.
An assertion is scaffolding on a building: invaluable while the work is going on, and -O is the crew that takes every pole down before the doors open. Nothing structural may rest on it.
saying these in an interview costs you the question
- Thinks a failed assertion raises ValueError, not AssertionError
- Uses assert to validate request payloads or user input
- Believes -O only speeds code up and leaves assertions running
- Puts a state-changing call inside the asserted expression
- Cannot say what __debug__ is or when it is False
- Thinks catching AssertionError makes the check dependable