#3995·Nuitka

list `.append()` through a conditional expression is silently dropped

Author: pimenoffdCreated Aug 12, 2026Updated Sep 5, 2026
Labelsbugfactoryexcellent_report

Thanks for Nuitka — we use it to ship a Python service as compiled modules, and it has been solid. This one bit us quietly, though, which is why I went to the trouble of narrowing it down.

When a list is appended to via (a if cond else b).append(x), and that list is only consumed locally afterwards, the compiled module behaves as if the append never happened. The element reaches neither list. No exception, no warning — the following if a: just doesn't run and the caller gets a wrong answer.

Minimal repro, standard library only:

python
class Item:
    def __init__(self, label, cap):
        self.label = label
        self.cap = cap


class Bundle:
    def __init__(self, items):
        self.items = items


def collect(bundle, score):
    hits: list[str] = []
    misses: list[str] = []
    out = []
    for item in bundle.items:
        if item.cap <= 3:
            (hits if score == item.cap else misses).append(item.label)
    if hits:
        out.append("|".join(x for x in hits))
    return out


def run():
    bundle = Bundle([Item("dropped", 7), Item("kept", 3)])
    return collect(bundle, 3.0)
bash
# interpreted
$ python -c "import mre; print(mre.run())"
['kept']

# compiled
$ python -m nuitka --module mre.py && rm mre.py
$ python -c "import mre; print(mre.run())"
[]

The tell

The conditional selects hits, not misses — rewriting that one line as a plain if/else appending to hits makes the compiled build agree with the interpreter again. That's also the workaround we're carrying.

More pointedly: if you delete misses entirely so the name is undefined anywhere in the file, the compiled build still doesn't raise NameError. It appends to neither branch.

What is and isn't needed

Each of these was a separate build-and-run.

Required: the list must not escape the function. Replacing the if hits: block with return hits makes the bug disappear. Worth flagging because it makes this awkward to stumble into with a hand-written test — the natural way to write a small check is to return the list, and that hides it. Several of my early attempts at a minimal repro failed for exactly that reason and misled me into thinking the idiom was innocent.

Not required — still reproduces with each of these removed:

  • any third-party package (originally found in code using pydantic models; plain classes reproduce it identically)
  • a second occurrence of the same idiom elsewhere in the function, including unreachable code
  • a conditional expression used as the if condition
  • a generator expression reusing the enclosing loop variable's name
  • getattr() indirection on the attributes involved

Not claiming a mechanism

I haven't read the generated C or Nuitka's IR. "The list is inferred to be empty and the branch is dropped" fits what I see, but I'm reporting observed behaviour only.

Existing issues

I searched and didn't find this one already reported — closest I got was a few in the same family, which may or may not be useful to you:

  • #3860 — getattr(obj, name) in a for loop returning a stale obj under --onefile. Same shape as this: a loop, a silently wrong value, no crash. Different mode and different construct, so I don't think it's the same root cause.
  • #3973 — async generator expression miscompiled. Unrelated construct, but also codegen producing behaviour that diverges from the interpreter without any error.

If either turns out to share a cause with this, apologies for the extra issue.

Environment

Reproduces on both stable and develop.

python -m nuitka --version on 4.1.3 (pip):

4.1.3
Commercial: None
Python: 3.12.13 (main, May 10 2026, 19:20:41) [Clang 22.1.3 ]
Flavor: Python Build Standalone
Executable: /.../ddmin-env/bin/python
OS: Darwin
Arch: arm64
macOSRelease: 26.5.2
Version C compiler: clang (clang 21.0.0).

python -m nuitka --version on develop (pip install git+https://github.com/Nuitka/Nuitka.git@develop):

4.2rc5
Update status: newer than stable release '4.1.3' (cached, 26 seconds old).
Commercial: None
Python: 3.12.13 (main, May 10 2026, 19:20:41) [Clang 22.1.3 ]
Flavor: Python Build Standalone
Executable: /.../nuitka-dev-env/bin/python
OS: Darwin
Arch: arm64
macOSRelease: 26.5.2
Version C compiler: clang (clang 21.0.0).
  • Python installed via uv (Python Build Standalone), venv used
  • Also reproduces under CPython 3.11 on Debian slim / linux-amd64 with gcc, inside a Docker build — so not specific to macOS, arm64, or clang
  • No third-party packages involved in the repro above
  • Runs correctly under plain CPython in every case; the divergence appears only after compilation

Where we hit it

In a service that scores records and publishes a list of reasons each one was flagged. The list is built exactly like hits above and then rendered only if non-empty. Compiled, it always came out empty, so the published output stated there were no reasons — for records that had them. Nothing crashed and nothing was logged.

Our test suite stayed green throughout, because it runs against the plain .py while the artifact we ship is compiled. We only caught it by running the same suite inside the compiled image. That combination — wrong values instead of a crash, plus tests that structurally cannot observe it — is what made it expensive.

Happy to test a fix against the real application if that's useful.