assert isinstance(flag, bool) reads like a check. Under python -O it is not there at all, and nothing at the call site says so. The usual advice is to stop writing safety checks as asserts. There is a better answer: let the module refuse to be imported in the mode where its checks evaporate.
if not __debug__:
raise RuntimeError("this module's checks are asserts; refusing to run with -O")
$ python useguard.py
guard loaded, gate(True) = True
$ python -O useguard.py; echo $?
RuntimeError: this module's checks are asserts; refusing to run with -O
1
Five lines, and the failure moves from the approval to the import. A deployment that will not start is a different kind of problem from a gate that quietly stopped gating.
What the runtime will and will not tell you
Two different questions get confused here, and they have different answers.
What has this image been imported under? The optimization level is stamped into the bytecode cache filename, so the artifacts accumulate:
$ python use.py && python -O use.py && python -OO use.py
$ ls __pycache__
m.cpython-314.opt-1.pyc
m.cpython-314.opt-2.pyc
m.cpython-314.pyc
That is real evidence and it survives the process that made it. It is also easy to lose:
$ rm -rf __pycache__ && PYTHONDONTWRITEBYTECODE=1 python -O use.py
$ ls __pycache__
(no such directory)
A read-only image layer does the same thing. So the artifact answers a forensic question, and answers it only sometimes.
What is this process running under right now? That one is reliable:
$ python probe.py
sys.flags.optimize = 0 | __debug__ = True
$ python -O probe.py
sys.flags.optimize = 1 | __debug__ = False
| question | source | can it be missing? |
|---|---|---|
| what has been imported | __pycache__/*.opt-N.pyc |
yes: PYTHONDONTWRITEBYTECODE, or a read-only layer |
| what is running now |
sys.flags.optimize, __debug__
|
no |
The guard uses the second one — but not the way this table implies, and the difference matters.
Correction (2026-09-10). @vinhnguyenthanhdn pointed out that __debug__ is folded to a constant when a module is compiled, so the guard is not consulting the running process at all. It is asking about its own bytecode. Reproduced here on 3.12.10, sourceless (compile, copy the pyc beside nothing, delete the source):
$ # pyc compiled optimize=1, imported by plain python
process __debug__ = True | sys.flags.optimize = 0
guard: RAISED
$ # pyc compiled optimize=0, imported by python -O
process __debug__ = False | sys.flags.optimize = 1
guard: silent
assert inside the module: FIRED
$ # control, same module with its source present
plain: guard silent, assert fires
-O: guard RAISED
Two consequences.
The row above that reads "what is running now" is true of sys.flags.optimize and false of the __debug__ the guard sees. Two different questions under one name, which is the same shape of confusion this post is about.
And the invariant I claimed was too wide. I wrote that the module refuses the mode that deletes its checks, which sounds like a statement about the process. The second row is the case that disproves it: the process is at -O, the guard says nothing, and the assert fires anyway, because that module's checks were never stripped. The guard was right to stay quiet.
The true statement is per-module, not per-process: a module never runs with its own checks stripped. Narrower than "nothing runs unchecked under -O", and stronger for a sourceless distribution, where the process flag tells you nothing about what is inside the pyc you shipped.
Why "just don't use asserts" is the weaker rule
It is advice to every future author of every line in the module, and review is what enforces it. The import guard is one line. The interpreter enforces that one, including over asserts nobody has written yet.
It also fails in the right direction. Someone deploying with -O for speed gets a crash with a sentence explaining the conflict, at startup, in their own terminal. The alternative is that the same person deploys successfully and finds out later, from an action nobody approved.
What it does not do
It does not make asserts a good way to express a safety invariant. If the check matters, if not isinstance(...): raise is still better, and the guard is what you put on top of the module while the asserts are still in it.
It also stops at the module boundary. A module with the guard is safe under -O; a module in the same process without it is not, and nothing coordinates the two.
Both of those cost more than five lines. This costs five.
Measured on CPython 3.14.4 (Windows) and cross-checked against 3.14.6 on macOS by @vinhnguyenthanhdn, who pointed out the __pycache__ artifact in the first place. The three-file listing and the PYTHONDONTWRITEBYTECODE result are reproduced from runs on both.
Top comments (2)
The guard is stronger than your table says, and for a reason the table does not have a row for:
__debug__is folded at compile time, so the guard is a property of the bytecode rather than of the process. Both directions on 3.14.6, shipping only a sourceless.pycwith no source next to it:So the guard fires exactly when the asserts in that same bytecode are gone, and stays quiet whenever they are present, which is a tighter coupling than consulting the live process would give you. It does cost you one row though: for a sourceless distribution, "what is running now" is not the thing the guard reads, so setting
PYTHONOPTIMIZEat deploy time against pre-compiled files does not trip it, and it also should not, because the asserts it defends were settled by the same compile.Both of your rows reproduce, and the correction is bigger than a missing row. My table is wrong about what the guard reads.
3.12.10 here, a different minor from your 3.14.6. Sourceless: compile, copy the pyc beside nothing, delete the source.
The control is there because A and B on their own could be a broken guard rather than a compile-time one; with the source present it behaves the ordinary way in both directions, so the difference is the sourceless pyc and not the guard.
Where my table is wrong
I wrote that
sys.flags.optimizeand__debug__answer "what is running now" and cannot be missing. The second half is true ofsys.flags.optimize. It is not true of the__debug__the guard reads, because that one is not read at all — it is folded to a constant when the module is compiled, so the guard is asking about its own bytecode. Two different questions wearing one name, which is exactly the shape of thing the post was supposed to be about.And the invariant is better than the one I claimed
I described this as "the module refuses the mode that deletes its checks", which reads as a statement about the process. Your row B is the case that shows it is not: the process is at
-O, the guard says nothing, and the assert fires anyway, because that module's checks were never stripped. The guard was right to stay quiet.So the honest statement is per-module rather than per-process: a module never runs with its own checks stripped. That is narrower than "nothing runs unchecked under -O" and it is the thing that is actually true. It is also the stronger guarantee for a sourceless distribution, where the process flag tells you nothing about what is in the pyc you shipped.
I will correct the article on both points and cite you.