Skip to content

[mypyc] Fix ClassVar self-references in class bodies#21011

Open
VaggelisD wants to merge 2 commits intopython:masterfrom
VaggelisD:fix-classvar-self-reference
Open

[mypyc] Fix ClassVar self-references in class bodies#21011
VaggelisD wants to merge 2 commits intopython:masterfrom
VaggelisD:fix-classvar-self-reference

Conversation

@VaggelisD
Copy link
Contributor

@VaggelisD VaggelisD commented Mar 12, 2026

Fixes mypyc/mypyc#972.

In CPython, the class body executes as a function where earlier assignments are available to later ones e.g this is possible:

class Foo:
  A: t.ClassVar = {1, 2, 3}
  B: t.ClassVar = {4, 5, 6}
  C: t.ClassVar = A | B

mypyc previously resolved such names via load_global(), looking them up in the module globals dict where they don't exist causing a KeyError at runtime.

This PR fixes this by tracking ClassVar names as they're defined during class body processing, and redirecting lookups to the class being built: the type object (py_get_attr) for extension classes, or the class dict (dict_get_item_op) for non-extension classes.

VaggelisD and others added 2 commits March 12, 2026 15:37
In CPython, the class body executes as a function where earlier
assignments are available to later ones (e.g. C = A | B where A
is a ClassVar defined earlier in the same class). mypyc previously
resolved such names via load_global(), looking them up in the module
globals dict where they don't exist — causing a KeyError at runtime.

Fix by tracking ClassVar names as they're defined during class body
processing, and redirecting lookups to the class being built: the
type object (py_get_attr) for extension classes, or the class dict
(dict_get_item_op) for non-extension classes.

This enables patterns like:
    class Parser:
        TYPE_TOKENS: ClassVar = {"INT", "VARCHAR"}
        FUNC_TOKENS: ClassVar = TYPE_TOKENS | {"FUNCTION"}
@ilevkivskyi
Copy link
Member

IIUC this fixes mypyc/mypyc#972. If yes, could you please updated PR description to start with "Fixes mypyc/mypyc#972"?

@ilevkivskyi
Copy link
Member

Btw thanks for all the PRs! Please note that we will have a release soon, so if you want these PRs included in v1.20, please ping @JukkaL (who will be release manager).

@VaggelisD
Copy link
Contributor Author

Hey @ilevkivskyi, we actually ran into this in independently in SQLGlot, thus the dict-based repro instead of the much simpler b=a as was noted in the issue. Thanks for the heads up, updated the description.


Btw thanks for all the PRs

Thank you for the project! We were able to make SQLGlot faster by 4-5x on avg and we only just got started. Hopefully we will be contributing even more fixes and features.


Regarding pinging, is asking for a review in Github the preferred way? I'm sure the core contributors are oversubscribed so I've avoided pinging in general. I had also joined the Gitter to discuss these solutions but I'm not sure if it's active anymore?

@ilevkivskyi
Copy link
Member

Regarding pinging, is asking for a review in Github the preferred way?

Yeah, you can request review or @-mention. It is true that we are quite busy here, but I for example don't mind being pinged sometimes, since I sometimes simply forget about a PR.

I had also joined the Gitter to discuss these solutions but I'm not sure if it's active anymore?

Yeah, Gitter is not active. We want to create a new Discord server at some point soon, but didn't get to this yet.

@VaggelisD
Copy link
Contributor Author

Yeah, Gitter is not active. We want to create a new Discord server at some point soon, but didn't get to this yet.

Stating the obvious here but a Discord would be awesome; I can vouch that having to manage a community channel is not easy but it'd be of great help for external contributors, especially for more complex PRs in which upfront coordination & design can massively reduce PR friction later down the road. We'll jump in when it's ready!

Copy link
Collaborator

@p-sawicki p-sawicki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be good to add irbuild tests with the new method calls to confirm they are generated as expected.

Comment on lines +235 to +241
# Class body context: tracks ClassVar names defined so far when processing
# a class body, so that intra-class references (e.g. C = A | B where A is
# a ClassVar defined earlier in the same class) can be resolved correctly.
# Without this, mypyc looks up such names in module globals, which fails.
self.class_body_classvars: dict[str, None] = {}
self.class_body_obj: Value | None = None
self.class_body_is_ext: bool = False
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to move these instance variables to ClassIR and add a pointer in IRBuilder to the IR that's currently processed? seems like that could simplify things a bit, since for example class_body_is_ext effectively duplicates ClassIR.is_ext_class.

Comment on lines +202 to +205
# Restore previous class body context (handles nested classes).
builder.class_body_classvars = saved_classvars
builder.class_body_obj = saved_obj
builder.class_body_is_ext = saved_is_ext
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nested classes are currently not supported (they should be rejected on line 103) so this isn't needed.

Comment on lines +5825 to +5836
@mypyc_attr(allow_interpreted_subclasses=True)
class NonExt:
A: ClassVar[Set[str]] = {"a", "b"}
B: ClassVar[Set[str]] = {"c"}
C: ClassVar[Set[str]] = A | B

@mypyc_attr(allow_interpreted_subclasses=True)
class NonExtDict:
BASE: ClassVar[Dict[str, int]] = {"x": 1}
EXTENDED: ClassVar[Dict[str, int]] = {**BASE, "y": 2}

@mypyc_attr(allow_interpreted_subclasses=True)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

native classes may allow interpreted subclasses so this isn't enough to make these non-ext. you need @mypyc_attr(native_class=False).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can't access ClassVars directly in the class body

3 participants