Environment- Metashape Professional 2.3.1 (build 22580), standalone Python module
- Linux x86_64, wheel metashape-2.3.1-cp39.cp310.cp311.cp312.cp313-abi3-linux_x86_64.whl
- CPython 3.11 (see "CPython version dependency" below — 3.12+ masks it)
SummaryComparing
None (or any non-Metashape object) with a Metashape object using
== /
!= — for example
None == chunk.cameras[0].group — returns the correct boolean but
leaks a decref on the NotImplemented singleton. Each such comparison lowers
NotImplemented's reference count by one; once it reaches zero the interpreter aborts:
Fatal Python error: notimplemented_dealloc: deallocating NotImplemented(SIGABRT, process exit 134.)
Minimal reproductionRun in a Metashape 2.3.1 environment on
CPython < 3.12 (e.g. 3.11) — on CPython >= 3.12 the crash is masked (see "CPython version dependency" below):
import sys
import Metashape
g = Metashape.Document().addChunk().addCameraGroup()
print("Metashape", Metashape.version, "| Python", sys.version.split()[0])
print("NotImplemented refcount:", sys.getrefcount(NotImplemented))
for i in range(1, 21):
_ = None == g # returns False, but leaks one reference on NotImplemented
print(f"after {i}: NotImplemented refcount = {sys.getrefcount(NotImplemented)}")On CPython 3.11 the printed refcount decreases by one per iteration and the process aborts with
notimplemented_dealloc when it reaches zero (typically within ~5 iterations). The comparison result itself is correct (
False) — the crash comes purely from the refcount corruption.
Example output (3.11):
Metashape 2.3.1.22580 | Python 3.11.15
NotImplemented refcount: 5
after 1: NotImplemented refcount = 4
after 2: NotImplemented refcount = 3
after 3: NotImplemented refcount = 2
Fatal Python error: notimplemented_dealloc: deallocating NotImplementedExpected vs. actual- Expected: None == <Metashape object> returns False and leaves NotImplemented's refcount unchanged (the rich-comparison protocol borrows the reference to NotImplemented; it must not be stolen).
- Actual: the refcount is decremented on every such comparison, eventually deallocating the singleton and aborting the interpreter.
Root cause (analysis)None == g evaluates
None.__eq__(g), which returns
NotImplemented; CPython then tries the reflected
g.__eq__(None). The Metashape C-extension's reflected comparison appears to
over-decref the
NotImplemented it handles (treating a borrowed reference as owned). It reproduces with any Metashape object type compared against
None (tested with
CameraGroup; the same pattern applies to markers, sensors, components, cameras, etc.). The module also over-decrefs
None itself in some comparison paths (same failure mode as
none_dealloc).
CPython version dependency- CPython < 3.12 — NotImplemented is an ordinary singleton with a small refcount, so the leak reaches zero and the interpreter aborts. A single comparison only makes it off-by-one, so the abort is deferred and can surface at a much later, unrelated point in the program.
- CPython >= 3.12 (immortal objects, PEP 683) — NotImplemented is immortal (sys.getrefcount(NotImplemented) reports 4294967295), so incref/decref are no-ops and the leak is masked (no crash). The underlying refcount bug is still present, just harmless.
ImpactOn CPython < 3.12, any code that compares a Metashape object with
== /
!= where an operand is
None will intermittently abort. This is easy to hit unintentionally — e.g. iterating cameras and testing
camera.group == some_group while some cameras are ungrouped (
camera.group is None), which produces
None == some_group.
WorkaroundsOn CPython 3.11 (where the crash occurs)- Avoid == / != on Metashape objects in your own code. Use is / is not for None checks and compare entities by their integer .key:
# instead of: if camera.group == group:
if camera.group is not None and camera.group.key == group.key:
Removes the leak triggers in code you control (not any comparison inside the module itself). - Re-add the dropped reference via ctypes right after a known-leaking comparison (brittle; only if you can pinpoint every call):
import ctypes
_ = None == some_metashape_obj
ctypes.pythonapi.Py_IncRef(ctypes.py_object(NotImplemented)) - Immortalize the singletons at startup (last resort) — mimic CPython >= 3.12 by inflating the refcounts once so the leak can never reach zero:
import ctypes
for obj in (None, NotImplemented):
ctypes.c_ssize_t.from_address(id(obj)).value += 1 << 30
Effective but pokes at CPython internals and only masks the bug — use with care.
Recommended: run on CPython >= 3.12On CPython >= 3.12,
None and
NotImplemented are immortal (
PEP 683), so the leaked decrefs are no-ops and no code changes are needed.
The real fix is for the Metashape C-extension to stop stealing the borrowed
NotImplemented (and
None) reference in its comparison paths.