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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions