len() and bool() of a list built via `(a if cond else b).append(x)` constant-folded to 0 / False (with --enable-plugin=pyside6 and a QApplication)
Bug Report
Bug Description
When a list is populated through a conditional expression that selects the list object, followed by a mutating method call — (a if cond else b).append(x) — the list's actual contents are correct at runtime (its repr shows all elements), but len(b) and bool(b) are compiled as if the list were empty: len(b) returns 0 and if b: takes the else branch.
In other words, the list-size analysis appears to lose the append() that happens through the conditional-expression (phi) node, and the len/truth-value of the list is then constant-folded to 0/False.
This is a silent miscompilation: no warning, no error, the program just takes wrong branches. We first saw it in a shipping release build as a UI bug (a "these features are unsupported" section of a dialog silently disappeared in compiled builds only); reducing it produced the SSCCE below.
️ Environment
1. Nuitka Version, Python Version, OS, and Platform
Full output of python -m nuitka --version:
4.1.2
Commercial: None
Python: 3.13.6 (main, Aug 8 2025, 17:02:53) [MSC v.1944 64 bit (AMD64)]
Flavor: Unknown
GIL: yes
Executable: F:\code\desktop_app\.venv\Scripts\python.exe
OS: Windows
Arch: x86_64
WindowsRelease: 11
Version C compiler: ~\AppData\Local\Nuitka\Nuitka\Cache\DOWNLO~1\pip\private-6533d32b\Lib\site-packages\ziglang\zig.exe (zig.exe 0.16.0).2. How Nuitka and Python were installed
Python 3.13.6 standalone (python.org installer), Nuitka installed via pip into a uv-managed project virtualenv (uv sync, dependency declared in pyproject.toml).
3. Relevant PyPI Packages and Versions
Only PySide6 is involved in the reproducer:
Nuitka 4.1.2 uv
PySide6 6.11.1 uv
PySide6_Addons 6.11.1 uv
PySide6_Essentials 6.11.1 uv
shiboken6 6.11.1 uv(python -m nuitka --list-distribution-metadata, shortened to relevant entries.)
️ To Reproduce
1. "Hello World" Test
Not applicable — basic compilation works fine; the issue is a codegen miscompilation of specific list operations.
2. Short, Self-Contained, Correct, Eligible (SSCCE) Example
Two files in one directory. gmlike.py (predicate module):
from dataclasses import dataclass
from enum import StrEnum
class Capability(StrEnum):
NUKE = "nuke"
SUPER_NUKE = "super_nuke"
STORM = "full_screen"
LIGHTNING = "full_screen_lightning"
COMBO = "combo_multiplier"
ATTACK_LEVEL = "attack_level"
ATTACK_DOUBLE = "attack_double"
CHARGE = "charge"
REPUTATION = "reputation"
MYSTERY = "mystery"
HERO = "hero_compose"
RED_PACKET = "red_packet"
_ALL = frozenset(Capability)
_ALMOST_ALL = _ALL - {
Capability.ATTACK_LEVEL,
Capability.ATTACK_DOUBLE,
Capability.LIGHTNING,
}
GIFT_CATEGORY_TO_CAPABILITY: dict[str, Capability] = {
"nuke": Capability.NUKE,
"super_nuke": Capability.SUPER_NUKE,
"storm": Capability.STORM,
"lightning": Capability.LIGHTNING,
"combo_multiplier": Capability.COMBO,
"attack_level": Capability.ATTACK_LEVEL,
"attack_double": Capability.ATTACK_DOUBLE,
"charge": Capability.CHARGE,
"reputation": Capability.REPUTATION,
}
GIFT_CATEGORY_TO_CAPABILITIES: dict[str, frozenset[Capability]] = {
cat: frozenset({cap}) for cat, cap in GIFT_CATEGORY_TO_CAPABILITY.items()
}
@dataclass(frozen=True)
class GameModeSpec:
capabilities: frozenset[Capability]
def supports(self, cap: Capability) -> bool:
return cap in self.capabilities
SPECS = {"ds": GameModeSpec(capabilities=_ALMOST_ALL)}
def resolve_spec(mode: str) -> GameModeSpec:
return SPECS.get(mode, SPECS["ds"])
def spec_supports_category(spec: "GameModeSpec", category: str) -> bool:
caps = GIFT_CATEGORY_TO_CAPABILITIES.get(category)
if caps is None:
return False
return any(spec.supports(cap) for cap in caps)issue_repro_d.py (entry point):
# nuitka-project: --standalone
# nuitka-project: --enable-plugin=pyside6
# nuitka-project: --assume-yes-for-downloads
# nuitka-project: --windows-console-mode=force
import os
os.environ["QT_QPA_PLATFORM"] = "offscreen"
from PySide6.QtWidgets import QApplication
app = QApplication([])
from gmlike import GIFT_CATEGORY_TO_CAPABILITY, resolve_spec, spec_supports_category
LINES = []
def refresh_lists(spec):
supported: list[str] = []
unsupported: list[str] = []
for category in GIFT_CATEGORY_TO_CAPABILITY:
(supported if spec_supports_category(spec, category) else unsupported).append(
category
)
LINES.append(f"unsup={unsupported} len={len(unsupported)} bool={bool(unsupported)}")
if unsupported:
LINES.append("IF entered")
else:
LINES.append("ELSE entered")
refresh_lists(resolve_spec("ds"))
result = "\n".join(LINES)
print(result)
with open("issue_repro_d_result.txt", "w", encoding="utf-8") as f:
f.write(result + "\n")3. Nuitka Command Line Options
python -m nuitka --standalone --enable-plugin=pyside6 --assume-yes-for-downloads --windows-console-mode=force issue_repro_d.py(The same options are embedded as # nuitka-project: comments in the entry file. No onefile, no --deployment, no --quiet.)
Expected Behavior
Running issue_repro_d.py with CPython prints:
unsup=['lightning', 'attack_level', 'attack_double'] len=3 bool=True
IF enteredThe compiled program should behave identically: 3 items end up in unsupported, so len(unsupported) == 3, bool(unsupported) is True, and the if unsupported: branch is taken.
Actual Behavior & Output
Running the Nuitka-compiled issue_repro_d.exe prints:
unsup=['lightning', 'attack_level', 'attack_double'] len=0 bool=False
ELSE enteredNote the contradiction within a single line: the list's repr contains 3 elements, yet len() returns 0 — the runtime list is fine, but the len/truth-value computations were folded at compile time based on a stale "always empty" shape analysis.
↩️ Regression
Unknown. 4.1.2 is the version our project has pinned; we did not have this code pattern in compiled builds before, so there is no earlier known-good data point from our side.
Additional Context
What we observed while reducing the reproducer (hopefully useful for isolating the cause):
The conditional expression selecting the list is required. Rewriting the loop body as an explicit
if/elsewith two separate.append()calls fixes it (that's our current workaround):for category in GIFT_CATEGORY_TO_CAPABILITY: if spec_supports_category(spec, category): supported.append(category) else: unsupported.append(category)QApplication(...)being instantiated (with--enable-plugin=pyside6) is required for this reduced reproducer; the predicate living in another module is also part of the trigger. The same single-file loop with a local predicate and no Qt ((a if i % 2 == 0 else b).append(i)followed byif b:) compiles correctly.Printing the list itself always shows correct contents; only
len()/bool()/ truth tests are wrong. In our original application the symptom was slightly different:if unsupported:always took the else branch whilelen(unsupported)stayed correct — so the exact set of folded operations can vary with the surrounding code.Caveat: I have not tested the
developbranch; if you can't reproduce ondevelopI'm happy to test any patch build.
Source: Nuitka/Nuitka