skip to content

How can Python code tell whether it was started with -O or PYTHONOPTIMIZE?

level: middleimportance: should knowfreq 26%

answer

  1. A namedtuple of interpreter startup flags
  2. One integer, however the level was set
  3. The environment is only half the story
  4. A constant that is False at level one or higher
  5. sys.flags.optimize, and opt-N cache tags

basics

~20 s

Read sys.flags.optimize, which is the integer level 0, 1 or 2 however it was set. The built-in debug is False whenever the level is at least 1. Reading the environment variable alone misses a command-line -O.

solid answer

~50 s

`sys.flags.optimize` is the authoritative answer: it holds the effective integer level for the process, whether it came from `-O` on the command line, from `-OO`, or from `PYTHONOPTIMIZE` in the environment. Checking `os.environ` instead is a classic mistake, because it sees only the environment half of that. The built-in `__debug__` tells you the same thing coarsely — it is `False` at level 1 or higher — but it cannot distinguish level 1 from level 2, so a check that cares about docstrings must read `sys.flags.optimize`. `PYTHONOPTIMIZE` carries the level as an integer, so `PYTHONOPTIMIZE=2` matches `-OO`; a non-numeric value is treated as level 1. Because the level changes the emitted bytecode, cached files are tagged with it — `opt-1` and `opt-2` in the file name, no tag at level 0 — which is why the three levels never share a cache file.

code

console · 2 lines
console
$ PYTHONOPTIMIZE=2 python3 -c "import sys; print(sys.flags.optimize, __debug__)"
2 False

go deeper

for a junior

Know that sys.flags.optimize gives the level as 0, 1 or 2, and that debug is False whenever the level is at least 1. That pair answers the question at this level.

for a middle

Explain that sys.flags.optimize is normalised across the command line and PYTHONOPTIMIZE, that the variable carries an integer level, and that debug cannot separate level 1 from level 2.

for a senior

Demonstrate the operational habit: log the level at service startup, and explain why cached bytecode is tagged opt-1 and opt-2 so a level change cannot leave you executing bytecode compiled under a different one.

for a principal

Own the standard: which level the fleet runs, where it is set — image, orchestrator or entry point — and how build-time precompilation is aligned with it so shipped caches are the ones actually used.

Two questions hide inside this one: how does a running program discover its own optimization level, and how does the interpreter keep code compiled at different levels apart? Both matter in production, because the level is a process-wide property fixed at startup that silently changes what your code means. ## Reading the level `sys.flags` is a named tuple of the interpreter's command-line and environment flags, and `sys.flags.optimize` is the effective integer level: `0` by default, `1` for `-O`, `2` for `-OO`. It is normalised, so it reports the same value regardless of whether the level arrived on the command line or through the environment. That is the reason to prefer it over reading an environment variable directly — `os.environ` will happily tell you `PYTHONOPTIMIZE` is unset in a process launched as `python -O app.py`. The built-in `__debug__` is the coarse form of the same information. It is `True` at level 0 and `False` at any higher level, and unlike `sys.flags.optimize` it is a compile-time constant, which is precisely what makes `if __debug__:` blocks free at level 1. It cannot tell you whether docstrings survived, because that depends on level 2 specifically. ```pycon >>> import sys >>> sys.flags.optimize, __debug__ (0, True) ``` A useful habit for any long-lived service: log `sys.flags.optimize` once at startup, alongside the interpreter version. It costs one line and it turns "assertions were off in that environment" from an archaeology exercise into a grep. ## Setting it from the environment `PYTHONOPTIMIZE` carries the level as an integer, so `PYTHONOPTIMIZE=2` is equivalent to `-OO`. A value that is not a valid non-negative integer is treated as level 1. This is how the flag most often ends up switched on in a container: not by anyone editing the entry point, but by a base image or an orchestrator setting the variable, which is exactly why the runtime check matters more than reading the deploy manifest. ## Why cached bytecode is tagged A code object compiled at level 1 is genuinely different from the same source compiled at level 0 — the assertions are missing. If both were cached under one file name, the first process to run would decide what the second one executed. The naming scheme avoids that by putting the level in the file name: an unoptimized cache has no tag, level 1 adds `opt-1`, level 2 adds `opt-2`. The standard library exposes the exact rule rather than making you assemble the string yourself: ```python import importlib.util print(importlib.util.cache_from_source("etl/export.py", optimization=1)) ``` That prints `etl/__pycache__/export.cpython-314.opt-1.pyc` on CPython 3.14. Level 0 gives `export.cpython-314.pyc` with no tag. The consequence in operation is reassuring: switching the flag on or off cannot make you run the wrong bytecode, it only means the first run at a new level has to compile the source again. ## Precompiling for a level If you build an image and want the caches to be present at the level you will actually run, compile them at that level explicitly. `py_compile.compile()` takes an `optimize` argument, and the `compileall` module's command-line interface takes `-o` and accepts it more than once to produce caches for several levels in one pass. Precompiling at level 0 and then running the service at level 1 is not wrong, it is just wasted work: the level-1 caches get built on first import anyway. ## What a good answer sounds like Name `sys.flags.optimize` first and say it is normalised across both sources. Mention `__debug__` as the coarse, compile-time form. Explain that the environment variable carries an integer level. Then close on the tagging: three levels, three cache file names, so they cannot be confused for one another. ## The failure this prevents in practice Consider a deployment where an image is built by running the application once to warm its caches, then run under `-O` at serve time. Without tagging, the warmed caches would be level-0 bytecode with assertions intact, and the serving process would execute them despite the flag — the flag would appear not to work, intermittently, depending on whether a cache file happened to exist. The tag removes that whole class of confusion: the level-1 process only ever considers `opt-1` files, so the worst outcome is a cache miss and a recompile. ## Not to be confused with the interpreter version tag The same file name carries two pieces of information: `export.cpython-314.opt-1.pyc` names both the interpreter (`cpython-314`) and the optimization level (`opt-1`). They are independent — an interpreter upgrade and a level change both invalidate a cache, for different reasons — and `importlib.util.cache_from_source()` assembles both parts for you, which is why building the name by hand in tooling is a mistake.

  • Why can't you disable assertions for just one module by assigning to __debug__?
    `__debug__` is a compile-time constant, so assigning to it is a `SyntaxError`, and its value was already inlined into every code object when they were compiled. The optimization level is fixed for the whole process at interpreter startup and applies uniformly to every module compiled in it, so there is no per-module or per-import override to reach for.
  • How do you precompile a source tree at a specific optimization level?
    Use the `compileall` module's command-line interface with `-o` to name the level, repeating the option to emit caches for more than one level in a single pass; `py_compile.compile()` takes the same thing as an `optimize` argument for a single file. Compile at the level you will actually run, otherwise the caches you shipped are ignored and rebuilt on first import.
  • If a level-1 cache exists but a level-0 process starts, what happens?
    The level-0 process looks for the untagged cache file, does not find the `opt-1` one relevant, and compiles the source again — writing its own untagged cache. The tags mean the levels are simply different artefacts side by side, so a level change can never cause the wrong bytecode to run; the only cost is one recompilation.

saying these in an interview costs you the question

  • Checks os.environ for PYTHONOPTIMIZE instead of sys.flags.optimize
  • Treats sys.flags.optimize as a boolean rather than a level
  • Uses __debug__ to distinguish level 1 from level 2
  • Thinks all optimization levels share one cached bytecode file
  • Claims the level can be changed after the interpreter starts
  • Assumes only the command line can set the optimization level

context