Skip to content

[mypyc] Incremental separate compilation misses recompiling subclass-only modules when a transitive base class changes #21742

Description

@geooo109

Bug Report
Under incremental mypyc compilation with separate=True, a module whose classes inherit from ancestors defined several modules up the chain can be left stale when a distant ancestor's method set changes. Mypy's incremental system only recompiles a module when a recorded dependency changes, and a dependency edge to an ancestor's module is created in cases such as an explicit import or a checked expression whose type references that ancestor. A bare subclass definition produces neither, so the transitive ancestors never enter the module's dependency graph.

For type checking this isn't a problem: a change in a distant ancestor doesn't affect the subclass's interface. But mypyc's generated C embeds the full layout of every ancestor into the subclass, including a vtable slot for each inherited method and struct offsets for inherited attributes. So the generated C depends on ancestors that the dependency graph doesn't capture.

Originally surfaced in a sqlglot CI build (tobymao/sqlglot#7875): https://github.com/tobymao/sqlglot/actions/runs/29492925074/job/87603095733

To Reproduce

# a.py
class A:
    def foo1(self) -> int:
        return 1
    def foo2(self) -> int:   # <- removed in the second build
        return 2

# b.py
from a import A

class B(A):
    def other(self) -> str:
        return "b"

# c.py
from b import B

class C(B): 
    pass   # <- records no dependency on a.py

Expected Behavior

The incremental rebuild regenerates c.py's C as well (its vtable references every inherited method of its ancestors) and the build succeeds.

Actual Behavior

python3 setup.py build_ext --inplace # first build, populates .mypy_cache — succeeds
now delete foo2 from a.py
python3 setup.py build_ext --inplace # incremental rebuild
build/__native_c.c:66:34: error: no member named 'CPyDef_a___A___foo2' in
'struct export_table_a'; did you mean 'CPyDef_a___A___foo1'?
1 error generated.
error: command '/usr/bin/clang' failed with exit code 1

Your Environment

  • Mypy version used: reproduced on a fork based on the release-1.20 branch
    (fork version 2.1.0.post8); the relevant dependency-recording code in
    State.finish_passes / patch_indirect_dependencies is unchanged on master
  • Mypy command-line flags: none — mypy is invoked by mypyc via
    mypycify(["a.py", "b.py", "c.py"], separate=True)
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: 3.12.9

I will link a PR for this soon.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrong

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions