py/compile: optionally build __annotations__ on classes (PEP 526)#19340
py/compile: optionally build __annotations__ on classes (PEP 526)#19340zacharywhitley wants to merge 1 commit into
Conversation
MicroPython currently parses class-body annotations (`x: int = 0`) but
discards the type expression entirely — see the comment in py/compile.c
("the annotation is in pns1->nodes[0] and is ignored"). This makes any
library that introspects field types silently see no fields:
typing.get_type_hints, dataclasses, pydantic, attrs.
This adds an opt-in `MICROPY_PY_BUILTINS_CLASS_ANNOTATIONS` config flag
(default off). When enabled, the compiler emits a class-body init that
seeds `__annotations__ = {}`, then at each class-scope annassign emits
`__annotations__[name] = type_expr`. The result matches CPython 3.0+
semantics.
Cost when enabled: one dict allocation per class definition + three
opcodes per annotated class member. Zero behavioural change when off.
mpy-cross opts into the flag so frozen-module workflows pick up
annotations from .py source. Other ports can opt in via their
mpconfigport.h.
Test: tests/basics/class_annotations.py — self-SKIPs if the flag is
off, byte-identical to CPython output when on. Verified against
CPython 3.13.
Side-effect check: annotations are evaluated exactly once at class
definition, matching CPython.
Subclass behaviour: `__annotations__` does NOT auto-merge from a base
— matches CPython, where typing.get_type_hints handles that walk.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #19340 +/- ##
=======================================
Coverage 98.51% 98.51%
=======================================
Files 176 176
Lines 22903 22903
=======================================
Hits 22562 22562
Misses 341 341 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Code size report: |
|
Hallo, Also I notice a comment in your PR that does not make sense in the context of MicroPython:
these are all CPython references , Not MicroPython. Could it be that this PR has been AI Generated ? |
|
They're discarded on purpose, see the discussion in #17643 (starting from comment #17643 (comment)). |
|
Thanks for the pointer @agatti. I think there's a code-path mismatch though — #17643 settled the parser-level question of whether to accept non- This PR doesn't touch that path. It changes a separate site in Default off; behaviour bit-identical to current MP when off; opt-in cost is one dict allocation per class + ~three opcodes per annotated member (CI's automated code-size report confirms zero delta on bare-arm / minimal x86 with the flag off). Tested as byte-identical to CPython 3.13 output when on. @Josverl — refiled the description against the required template above, and re-grounded the motivation in MicroPython-native usage (pure-MP Happy to address other concerns once the code-path distinction is clear. |
|
Thanks using the template. You state :
I’m not convinced by the current rationale as written. The PR motivation leans on “dataclasses-style” metaprogramming, which is not a strong MicroPython-centered justification especially as MicroPython to date does not support dataclasses. I would suggest to focus on concrete MicroPython goals, and design values, if possible point out specific MicroPython application, rather than Python generic, solutions that could benefit from this. But I could be overlooking patterns that can benefit from this, or just misunderstanding what your intent is, so please feel free to elaborate |
Summary
MicroPython's compiler currently parses class-body annotations (
x: int = 0inside a class body) but discards them — seepy/compile.c:This PR adds an opt-in
MICROPY_PY_BUILTINS_CLASS_ANNOTATIONSconfig flag (default off). When enabled, the compiler:__annotations__ = {}at class body entry.STORE_SUBSCRper annotated member, populating that dict withname -> type_expr.Output matches CPython 3.0+ —
cls.__annotations__returns the canonical dict. Behaviour is bit-identical to current MicroPython when the flag is off.Why preserve them? A growing body of pure-MicroPython libraries iterate over class annotations to drive metaprogramming — most prominently
dataclasses-style shims that infer field declaration order from__annotations__instead of requiring a separate_fields = [...]ladder. With the current discard, such shims silently see zero fields.This is orthogonal to the parser-level const-folding decision in #17643 (which dropped the
int-only check onvar: T = const(val)so any annotation now permits the fold). That path doesn't reach class scope and doesn't preserve the annotation — it just decides whether to fold the RHS. This PR touches a separate site inpy/compile.cthat emits class-bodyannassigninstructions.mpy-crossenables the flag so frozen-module workflows pick up annotations from.pysource at freeze time. Ports opt in via theirmpconfigport.h.Testing
tests/basics/class_annotations.py(new) — self-skips when the flag is off (no behaviour change to verify); when on, assertscls.__annotations__contents match CPython 3.13 across a single class, an inherited class, and a class with mixed annotated + default-value members.ports/unixin both flag states; the standard test suite is green in both configs.mpy-crosswith the flag on; frozen-module workflow tested on a downstream wasi port —.pysource compiles cleanly, annotations land in the generated.mpyand round-trip through import.Trade-offs and Alternatives
When the flag is off: zero behavioural and code-size change. Confirmed by CI's bare-arm / minimal x86 deltas being 0 bytes.
When the flag is on: one dict allocation per defined class + roughly three opcodes per annotated member. CI's code-size report shows mpy-cross +184 bytes (+0.048%) and unix x64 +184 bytes (+0.021%); other targets in the report carry the flag off and thus show 0 delta.
Alternative considered: storing annotations on the class object via a custom slot rather than a
__annotations__dict attribute. Rejected —__annotations__is the canonical contract consumers reach for viagetattr(cls, '__annotations__'). A custom slot would require every consumer to use a MicroPython-specific accessor and miss the point of the feature.Generative AI
I used generative AI tools when creating this PR, but a human has checked the code and is responsible for the code and the description above.