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, which is why it works in an image with no __pycache__ at all.
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 (0)