(mypyc) A mypyc-compiled class that inherits from Exception (any BaseException subclass) and has
instance attributes leaks memory on every instantiation — the instance's attribute storage (its
__dict__ contents) is never freed. The pure-Python version of the same class does not leak, and a
bare Exception subclass with no attributes does not leak. The leak is unbounded, so a long-running
process grows in RSS until it OOMs.
Encountered in an open-source project: sqlglot compiled via mypyc (sqlglot[c] / the sqlglotc
package — https://github.com/tobymao/sqlglot). Its ParseError is an Exception subclass with
attributes, so every raised-and-caught parse error leaks ~1 KB, OOM-ing long-running parsers.
To Reproduce
leakmod.py:
classBareErr(Exception): # no attributes -> no leakpassclassAttrErr(Exception): # has an instance attribute -> leaksdef__init__(self, msg: str) ->None:
self.data=msg
Compile with mypyc and run the driver (a mypy-play link isn't applicable — this is a mypyc runtime
issue, not a type-check result):
Constructing and discarding exception instances should not accumulate memory — RSS stays flat, as it
does for the pure-Python module.
Actual Behavior
Compiled with mypyc, AttrErr leaks ~400 bytes per instance (unbounded); pure Python does not:
# compiled (python -m mypyc leakmod.py)
BareErr: +0.1 MB
AttrErr: +16.1 MB # over 40k constructions; grows linearly, never plateaus
# pure python (same file, not compiled)
BareErr: +0.0 MB
AttrErr: +0.0 MB
Additional observations:
The leak happens on construction alone — no raise/except required.
sys.getrefcount on a fresh instance is normal and gc.get_objects() reports 0 live
instances, so the exception object is freed; it's the attribute storage (__dict__ contents,
i.e. str values) that leaks. Since str/dict-keys aren't tracked by the cyclic GC, the leak is
invisible to gc and tracemalloc; it's visible in RSS / memray / heaptrack.
gc.collect() and malloc_trim(0) do not reclaim it.
Your Environment
Mypy version used: 2.3.0+dev (current master); also affects released mypyc
Mypy command-line flags: compiled via python -m mypyc leakmod.py
Python version used: 3.11
Operating system: Linux (reproduced on x86_64 and aarch64)
Suspected root cause (from reading mypyc/codegen/emitclass.py)
Whether the class gets its own tp_dealloc / tp_clear / tp_traverse is gated on:
For a class inheriting Exception, cl.builtin_base == "PyBaseExceptionObject", so generate_full
is False; and has_managed_dict() returns False for PyBaseExceptionObject (~lines 1305-1308,
to avoid the Py_TPFLAGS_MANAGED_DICT / tp_dictoffset conflict). The gate is therefore False, so
mypyc neither generates nor installs its own dealloc/clear/traverse. The type falls back to
BaseException's inherited slots, which don't know about the subclass's __dict__ at the extra
tp_dictoffset (set at ~lines 322-338) — so the subclass's attribute storage is never freed.
Possibly a regression from #21290 ("Generate more type methods for types with managed dicts"), which
reworked the builtin_base dealloc path.
Notes from attempting a fix (to save you a dead end)
Naively enabling generation for builtin_base (or cl.builtin_base on the two gates) segfaults. gdb
points at the generated <Cls>_dealloc on its first PyObject_GC_UnTrack(self):
#0 _PyObject_GC_UNTRACK (op=<AttrErr ...>) at Include/internal/pycore_object.h:172
#1 PyObject_GC_UnTrack at Modules/gcmodule.c:2242
#2 AttrErr_dealloc () at build/__native.c
With the slots installed, the instances end up not GC-tracked (gc.is_tracked(AttrErr("x")) is
False), so the unconditional untrack in the builtin_base dealloc path (~lines 950-951) hits an
untracked object. The generated traverse/clear themselves look correct (they visit/clear the
__dict__ at sizeof(struct)). So a correct fix appears to need consistent
Py_TPFLAGS_HAVE_GC / GC-tracking handling for builtin_base instances, not just emitting the
functions.
The lifecycle around Exception would benefit from a repeated create/use/destroy regression test for “[mypyc] Memory leak: native attributes of compiled 'Exception' subclasses are never freed”. Checking that resources return to a stable baseline after several iterations would cover both the reported failure and a slower leak.
Activity
[mypyc] Fix memory leak in Exception subclasses with instance attributes
[mypyc] Fix memory leak in Exception subclasses with instance attributes
[mypyc] Fix memory leak in Exception and dict subclasses with instanc…
[mypyc] Fix memory leak in Exception and dict subclasses with instanc…
[mypyc] Fix memory leak in Exception and dict subclasses with instanc…
[mypyc] Fix Exception-subclass leak (round 3): re-widen + walk past h…
hiSandog commented on Jul 14, 2026
The lifecycle around Exception would benefit from a repeated create/use/destroy regression test for “[mypyc] Memory leak: native attributes of compiled 'Exception' subclasses are never freed”. Checking that resources return to a stable baseline after several iterations would cover both the reported failure and a slower leak.
[mypyc] Fix memory leak in `Exception` subclasses with instance attri…