[med-svn] [Git][python-team/packages/mypy][master] Cherry-pick patch from upstream to fix mypyc crash on Python 3.14.

Michael R. Crusoe (@crusoe) gitlab at salsa.debian.org
Thu Apr 23 18:14:13 BST 2026



Michael R. Crusoe pushed to branch master at Debian Python Team / packages / mypy


Commits:
f9b76c5c by Michael R. Crusoe at 2026-04-23T18:24:25+02:00
Cherry-pick patch from upstream to fix mypyc crash on Python 3.14.

- - - - -


3 changed files:

- debian/changelog
- + debian/patches/0001-mypyc-Generate-more-type-methods-for-types-with-mana.patch
- debian/patches/series


Changes:

=====================================
debian/changelog
=====================================
@@ -1,3 +1,9 @@
+mypy (1.20.2-2) UNRELEASED; urgency=medium
+
+  * Cherry-pick patch from upstream to fix mypyc crash on Python 3.14.
+
+ -- Michael R. Crusoe <crusoe at debian.org>  Thu, 23 Apr 2026 17:02:30 +0200
+
 mypy (1.20.2-1) unstable; urgency=medium
 
   * New upstream version


=====================================
debian/patches/0001-mypyc-Generate-more-type-methods-for-types-with-mana.patch
=====================================
@@ -0,0 +1,453 @@
+From ca800ef729855f8b73d561c34497ce0c91172376 Mon Sep 17 00:00:00 2001
+From: Piotr Sawicki <sawickipiotr at outlook.com>
+Date: Thu, 23 Apr 2026 16:03:11 +0200
+Subject: [mypyc] Generate more type methods for types with managed dicts (#21290)
+Origin: upstream, https://github.com/python/mypy/commit/c56ab5857047edac41db0826b2f83c5a3d476bc0
+
+Fixes #21133
+
+Types with the `Py_TPFLAGS_MANAGED_DICT` flag must call
+`PyObject_VisitManagedDict` and `PyObject_ClearManagedDict` in their
+`tp_traverse` / `tp_clear` functions according to
+[docs](https://docs.python.org/3/c-api/typeobj.html#c.Py_TPFLAGS_MANAGED_DICT)
+but the types generated by mypyc currently don't do this. We don't
+generate these functions at all so they get inherited from the base
+class.
+
+Failure to call these functions may result in a segfault in python 3.14
+when accessing the managed dict after its owner has been deallocated. I
+believe the crash happens because the logic for types with the
+`Py_TPFLAGS_INLINE_VALUES` flag in
+[`PyObject_ClearManagedDict`](https://github.com/python/cpython/blob/main/Objects/dictobject.c#L7803)
+is not run. The condition to add this flag has changed in
+[3.14](https://github.com/python/cpython/blob/3.14/Objects/typeobject.c#L8877)
+vs
+[3.13](https://github.com/python/cpython/blob/3.13/Objects/typeobject.c#L8171)
+so generated types with `Py_TPFLAGS_MANAGED_DICT` get it in 3.14.
+
+To fix, generate `tp_clear`, `tp_traverse`, and `tp_dealloc` for types
+with managed dicts. Also add a special case in these functions for
+classes with built-in bases to call the pointer of the base class.
+--- mypy.orig/mypyc/codegen/emit.py
++++ mypy/mypyc/codegen/emit.py
+@@ -1398,6 +1398,12 @@
+         self.emit_line(error_stmt)
+         return wrapper_name
+ 
++    def emit_base_tp_function_call(
++        self, derived_cl: ClassIR, tp_func: str, args: str, *, prefix: str = ""
++    ) -> None:
++        type_obj = self.type_struct_name(derived_cl)
++        self.emit_line(f"{prefix}{type_obj}->tp_base->{tp_func}({args});")
++
+ 
+ def c_array_initializer(components: list[str], *, indented: bool = False) -> str:
+     """Construct an initializer for a C array variable.
+--- mypy.orig/mypyc/codegen/emitclass.py
++++ mypy/mypyc/codegen/emitclass.py
+@@ -262,7 +262,8 @@
+     if not cl.builtin_base:
+         fields["tp_new"] = new_name
+ 
+-    if generate_full:
++    managed_dict = has_managed_dict(cl, emitter)
++    if generate_full or managed_dict:
+         fields["tp_dealloc"] = f"(destructor){name_prefix}_dealloc"
+         if not cl.is_acyclic:
+             fields["tp_traverse"] = f"(traverseproc){name_prefix}_traverse"
+@@ -335,6 +336,14 @@
+     else:
+         fields["tp_basicsize"] = base_size
+ 
++    if generate_full or managed_dict:
++        if not cl.is_acyclic:
++            generate_traverse_for_class(cl, traverse_name, emitter)
++            emit_line()
++        generate_clear_for_class(cl, clear_name, emitter)
++        emit_line()
++        generate_dealloc_for_class(cl, dealloc_name, clear_name, bool(del_method), emitter)
++        emit_line()
+     if generate_full:
+         assert cl.setup is not None
+         emitter.emit_line(native_function_header(cl.setup, emitter) + ";")
+@@ -345,13 +354,6 @@
+         init_fn = cl.get_method("__init__")
+         generate_new_for_class(cl, new_name, vtable_name, setup_name, init_fn, emitter)
+         emit_line()
+-        if not cl.is_acyclic:
+-            generate_traverse_for_class(cl, traverse_name, emitter)
+-            emit_line()
+-        generate_clear_for_class(cl, clear_name, emitter)
+-        emit_line()
+-        generate_dealloc_for_class(cl, dealloc_name, clear_name, bool(del_method), emitter)
+-        emit_line()
+ 
+         if cl.allow_interpreted_subclasses:
+             shadow_vtable_name: str | None = generate_vtables(
+@@ -380,7 +382,7 @@
+     emit_line()
+ 
+     flags = ["Py_TPFLAGS_DEFAULT", "Py_TPFLAGS_HEAPTYPE", "Py_TPFLAGS_BASETYPE"]
+-    if generate_full and not cl.is_acyclic:
++    if (generate_full or managed_dict) and not cl.is_acyclic:
+         flags.append("Py_TPFLAGS_HAVE_GC")
+     if cl.has_method("__call__"):
+         fields["tp_vectorcall_offset"] = "offsetof({}, vectorcall)".format(
+@@ -391,7 +393,7 @@
+             # This is just a placeholder to please CPython. It will be
+             # overridden during setup.
+             fields["tp_call"] = "PyVectorcall_Call"
+-    if has_managed_dict(cl, emitter):
++    if managed_dict:
+         flags.append("Py_TPFLAGS_MANAGED_DICT")
+     fields["tp_flags"] = " | ".join(flags)
+ 
+@@ -867,8 +869,14 @@
+     for base in reversed(cl.base_mro):
+         for attr, rtype in base.attributes.items():
+             emitter.emit_gc_visit(f"self->{emitter.attr(attr)}", rtype)
++    base_args = "(PyObject *)self, visit, arg"
++    emitter.emit_line("int rv = 0;")
++    if cl.builtin_base:
++        emitter.emit_base_tp_function_call(cl, "tp_traverse", base_args, prefix="rv = ")
++        emitter.emit_line("if (rv != 0) return rv;")
+     if has_managed_dict(cl, emitter):
+-        emitter.emit_line("PyObject_VisitManagedDict((PyObject *)self, visit, arg);")
++        emitter.emit_line(f"rv = PyObject_VisitManagedDict({base_args});")
++        emitter.emit_line("if (rv != 0) return rv;")
+     elif cl.has_dict:
+         struct_name = cl.struct_name(emitter.names)
+         # __dict__ lives right after the struct and __weakref__ lives right after that
+@@ -879,7 +887,7 @@
+             f"*((PyObject **)((char *)self + sizeof(PyObject *) + sizeof({struct_name})))",
+             object_rprimitive,
+         )
+-    emitter.emit_line("return 0;")
++    emitter.emit_line("return rv;")
+     emitter.emit_line("}")
+ 
+ 
+@@ -890,8 +898,11 @@
+     for base in reversed(cl.base_mro):
+         for attr, rtype in base.attributes.items():
+             emitter.emit_gc_clear(f"self->{emitter.attr(attr)}", rtype)
++    base_args = "(PyObject *)self"
++    if cl.builtin_base:
++        emitter.emit_base_tp_function_call(cl, "tp_clear", base_args)
+     if has_managed_dict(cl, emitter):
+-        emitter.emit_line("PyObject_ClearManagedDict((PyObject *)self);")
++        emitter.emit_line(f"PyObject_ClearManagedDict({base_args});")
+     elif cl.has_dict:
+         struct_name = cl.struct_name(emitter.names)
+         # __dict__ lives right after the struct and __weakref__ lives right after that
+@@ -935,6 +946,18 @@
+         emitter.emit_line("}")
+     if not cl.is_acyclic:
+         emitter.emit_line("PyObject_GC_UnTrack(self);")
++    if cl.builtin_base:
++        emitter.emit_line(f"{clear_func_name}(self);")
++        # For native subclasses of builtins such as dict, the base deallocator
++        # is responsible for tearing down base-owned storage and freeing memory.
++        # Re-track self if base is GC-aware to match cpython's subtype_dealloc.
++        base = f"{emitter.type_struct_name(cl)}->tp_base"
++        base_arg = "(PyObject *)self"
++        emitter.emit_line(f"if (PyType_IS_GC({base})) PyObject_GC_Track({base_arg});")
++        emitter.emit_base_tp_function_call(cl, "tp_dealloc", base_arg)
++        emitter.emit_line("done: ;")
++        emitter.emit_line("}")
++        return
+     if cl.reuse_freed_instance:
+         emit_reuse_dealloc(cl, emitter)
+     # The trashcan is needed to handle deep recursive deallocations
+--- mypy.orig/mypyc/irbuild/prepare.py
++++ mypy/mypyc/irbuild/prepare.py
+@@ -372,22 +372,6 @@
+     if attrs.get("acyclic") is True:
+         ir.is_acyclic = True
+ 
+-    free_list_len = attrs.get("free_list_len")
+-    if free_list_len is not None:
+-        line = attrs_lines["free_list_len"]
+-        if ir.is_trait:
+-            errors.error('"free_list_len" can\'t be used with traits', path, line)
+-        if ir.allow_interpreted_subclasses:
+-            errors.error(
+-                '"free_list_len" can\'t be used in a class that allows interpreted subclasses',
+-                path,
+-                line,
+-            )
+-        if free_list_len == 1:
+-            ir.reuse_freed_instance = True
+-        else:
+-            errors.error(f'Unsupported value for "free_list_len": {free_list_len}', path, line)
+-
+     # Check for subclassing from builtin types
+     for cls in info.mro:
+         # Special case exceptions and dicts
+@@ -416,6 +400,28 @@
+                     cdef.line,
+                 )
+ 
++    free_list_len = attrs.get("free_list_len")
++    if free_list_len is not None:
++        line = attrs_lines["free_list_len"]
++        if ir.is_trait:
++            errors.error('"free_list_len" can\'t be used with traits', path, line)
++        if ir.allow_interpreted_subclasses:
++            errors.error(
++                '"free_list_len" can\'t be used in a class that allows interpreted subclasses',
++                path,
++                line,
++            )
++        if ir.builtin_base:
++            errors.error(
++                '"free_list_len" can\'t be used in a class that inherits from a built-in type',
++                path,
++                line,
++            )
++        if free_list_len == 1:
++            ir.reuse_freed_instance = True
++        else:
++            errors.error(f'Unsupported value for "free_list_len": {free_list_len}', path, line)
++
+     # Set up the parent class
+     bases = [mapper.type_to_ir[base.type] for base in info.bases if base.type in mapper.type_to_ir]
+     if len(bases) > 1 and any(not c.is_trait for c in bases) and bases[0].is_trait:
+--- mypy.orig/mypyc/lib-rt/pythoncapi_compat.h
++++ mypy/mypyc/lib-rt/pythoncapi_compat.h
+@@ -925,7 +925,8 @@
+ {
+     PyObject **dict = _PyObject_GetDictPtr(obj);
+     if (dict == NULL || *dict == NULL) {
+-        return -1;
++        // Nothing to do.
++        return 0;
+     }
+     Py_VISIT(*dict);
+     return 0;
+--- mypy.orig/mypyc/test-data/fixtures/ir.py
++++ mypy/mypyc/test-data/fixtures/ir.py
+@@ -40,6 +40,7 @@
+ 
+ class object:
+     __class__: type
++    __dict__: dict[str, Any]
+     def __new__(cls) -> Self: pass
+     def __init__(self) -> None: pass
+     def __init_subclass__(cls, **kwargs: object) -> None: pass
+--- mypy.orig/mypyc/test-data/irbuild-classes.test
++++ mypy/mypyc/test-data/irbuild-classes.test
+@@ -2138,6 +2138,10 @@
+ class InterpSub:
+     pass
+ 
++ at mypyc_attr(free_list_len=1)  # E: "free_list_len" can't be used in a class that inherits from a built-in type
++class InheritsBuiltIn(dict):
++    pass
++
+ [case testUnsupportedGetAttr]
+ from mypy_extensions import mypyc_attr
+ 
+--- mypy.orig/mypyc/test-data/run-classes.test
++++ mypy/mypyc/test-data/run-classes.test
+@@ -3325,20 +3325,39 @@
+     assert(isinstance(d.fitem, ForwardDefinedClass))
+     assert(isinstance(d.fitems, ForwardDefinedClass))
+ 
+-[case testDelForDictSubclass-xfail]
+-# The crash in issue mypy#19175 is fixed.
+-# But, for classes that derive from built-in Python classes, user-defined __del__ method is not
+-# being invoked.
++[case testDelForDictSubclass]
++events: list[str] = []
++
++class Item:
++    def __del__(self) -> None:
++        events.append("deleting Item")
++
+ class DictSubclass(dict):
+-    def __del__(self):
+-        print("deleting DictSubclass...")
++    def __del__(self) -> None:
++        events.append("deleting DictSubclass")
++
++def test_dict_subclass_dealloc() -> None:
++    d = DictSubclass()
++    d["item"] = Item()
++    del d
+ 
+ [file driver.py]
+-import native
+-native.DictSubclass()
++import sys
++
++from native import events, test_dict_subclass_dealloc
++
++test_dict_subclass_dealloc()
++
++expected_events: list[str] = []
++
++# TODO: Fix when compiling for older python.
++# The user-defined __del__ method is currently only invoked when __dict__ is a managed dict
++# because calling __del__ in tp_clear on older python crashes.
++if sys.version_info >= (3, 12):
++    expected_events.append("deleting DictSubclass")
++expected_events.append("deleting Item")
+ 
+-[out]
+-deleting DictSubclass...
++assert events == expected_events, events
+ 
+ [case testDel]
+ class A:
+--- mypy.orig/mypyc/test-data/run-dicts.test
++++ mypy/mypyc/test-data/run-dicts.test
+@@ -368,3 +368,148 @@
+ [file userdefineddict.py]
+ class dict:
+     pass
++
++[case testDunderDictAccessAfterDel]
++from mypy_extensions import mypyc_attr
++
++ at mypyc_attr(allow_interpreted_subclasses=True)
++class NormDict(dict[str, str]):
++    def __init__(self, attr: int = 42) -> None:
++        super().__init__()
++        self.attr = attr
++
++class SubNormDict(NormDict):
++    def __init__(self, attr: int = 43) -> None:
++        super().__init__(attr)
++
++def test_dict_access() -> None:
++    n = NormDict(1)
++    d = n.__dict__
++    assert d["attr"] == 1
++    del n
++    assert d["attr"] == 1
++
++def test_subclass_dict_access() -> None:
++    s = SubNormDict(1)
++    d = s.__dict__
++    assert d["attr"] == 1
++    del s
++    assert d["attr"] == 1
++
++[file driver.py]
++from native import NormDict, SubNormDict, test_dict_access, test_subclass_dict_access
++
++
++class InterpretedNormDict(NormDict):
++    pass
++
++def test_dict_access_interpreted() -> None:
++    n = NormDict()
++    d = n.__dict__
++    assert d["attr"] == 42
++    del n
++    assert d["attr"] == 42
++
++def test_subclass_dict_access_interpreted() -> None:
++    s = SubNormDict()
++    d = s.__dict__
++    assert d["attr"] == 43
++    del s
++    assert d["attr"] == 43
++
++def test_allow_interpreted_subclass_dict_access() -> None:
++    s = InterpretedNormDict()
++    d = s.__dict__
++    assert d["attr"] == 42
++    del s
++    assert d["attr"] == 42
++
++test_dict_access()
++test_dict_access_interpreted()
++
++test_subclass_dict_access()
++test_subclass_dict_access_interpreted()
++test_allow_interpreted_subclass_dict_access()
++
++[fixture fixtures/typing-full.pyi]
++
++[case testCycleInDictSubclass]
++import gc
++from mypy_extensions import mypyc_attr
++
++events: list[str] = []
++
++ at mypyc_attr(allow_interpreted_subclasses=True)
++class CyclicDict(dict):
++    def __init__(self) -> None:
++        self["self"] = self
++
++    def __del__(self) -> None:
++        events.append("deleted")
++
++class SubCyclicDict(CyclicDict):
++    def __init__(self) -> None:
++        super().__init__()
++
++    def __del__(self) -> None:
++        events.append("sub deleted")
++
++def test_cyclic_dict_cleanup() -> None:
++    global events
++    events = []
++
++    c = CyclicDict()
++    del c
++    gc.collect()
++    assert events == ["deleted"], events
++
++def test_sub_cyclic_dict_cleanup() -> None:
++    global events
++    events = []
++
++    c = SubCyclicDict()
++    del c
++    gc.collect()
++    assert events == ["sub deleted"], events
++
++[file driver.py]
++import gc
++
++import native
++from native import CyclicDict, SubCyclicDict, test_cyclic_dict_cleanup, test_sub_cyclic_dict_cleanup
++
++
++class InterpretedCyclicDict(CyclicDict):
++    def __del__(self) -> None:
++        native.events.append("interpreted deleted")
++
++def test_cyclic_dict_cleanup_interpreted() -> None:
++    native.events = []
++
++    c = CyclicDict()
++    del c
++    gc.collect()
++    assert native.events == ["deleted"], events
++
++def test_sub_cyclic_dict_cleanup_interpreted() -> None:
++    native.events = []
++
++    c = SubCyclicDict()
++    del c
++    gc.collect()
++    assert native.events == ["sub deleted"], events
++
++def test_allow_interpreted_subclass_cyclic_dict_cleanup() -> None:
++    native.events = []
++
++    c = InterpretedCyclicDict()
++    del c
++    gc.collect()
++    assert native.events == ["interpreted deleted"], events
++
++test_cyclic_dict_cleanup()
++test_sub_cyclic_dict_cleanup()
++
++test_cyclic_dict_cleanup_interpreted()
++test_sub_cyclic_dict_cleanup_interpreted()
++test_allow_interpreted_subclass_cyclic_dict_cleanup()


=====================================
debian/patches/series
=====================================
@@ -1,3 +1,4 @@
+0001-mypyc-Generate-more-type-methods-for-types-with-mana.patch
 remove-mypyc-test-run-timeout
 hint-typeshed-package
 verbose



View it on GitLab: https://salsa.debian.org/python-team/packages/mypy/-/commit/f9b76c5cafb4ed6bf380784c7e48d66e1c8e5be3

-- 
View it on GitLab: https://salsa.debian.org/python-team/packages/mypy/-/commit/f9b76c5cafb4ed6bf380784c7e48d66e1c8e5be3
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20260423/a55d429d/attachment-0001.htm>


More information about the debian-med-commit mailing list