#4017·Nuitka

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)

Author: ciknightCreated Aug 27, 2026Updated Sep 11, 2026
Labelsbugduplicatefactoryexcellent_report

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):

python
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):

python
# 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

bash
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 entered

The 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 entered

Note 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/else with two separate .append() calls fixes it (that's our current workaround):

    python
    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 by if 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 while len(unsupported) stayed correct — so the exact set of folded operations can vary with the surrounding code.

  • Caveat: I have not tested the develop branch; if you can't reproduce on develop I'm happy to test any patch build.