Skip to content

py/compile: optionally build __annotations__ on classes (PEP 526)#19340

Open
zacharywhitley wants to merge 1 commit into
micropython:masterfrom
tegmentum:class-body-annotations
Open

py/compile: optionally build __annotations__ on classes (PEP 526)#19340
zacharywhitley wants to merge 1 commit into
micropython:masterfrom
tegmentum:class-body-annotations

Conversation

@zacharywhitley

@zacharywhitley zacharywhitley commented Jun 14, 2026

Copy link
Copy Markdown

Summary

MicroPython's compiler currently parses class-body annotations (x: int = 0 inside a class body) but discards them — see py/compile.c:

} else {
    // an annotation
    // the annotation is in pns1->nodes[0] and is ignored
    ...
}

This PR adds an opt-in MICROPY_PY_BUILTINS_CLASS_ANNOTATIONS config flag (default off). When enabled, the compiler:

  1. Seeds __annotations__ = {} at class body entry.
  2. Emits a STORE_SUBSCR per annotated member, populating that dict with name -> 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 on var: 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 in py/compile.c that emits class-body annassign instructions.

mpy-cross enables the flag so frozen-module workflows pick up annotations from .py source at freeze time. Ports opt in via their mpconfigport.h.

Testing

  • tests/basics/class_annotations.py (new) — self-skips when the flag is off (no behaviour change to verify); when on, asserts cls.__annotations__ contents match CPython 3.13 across a single class, an inherited class, and a class with mixed annotated + default-value members.
  • Built and ran ports/unix in both flag states; the standard test suite is green in both configs.
  • Built mpy-cross with the flag on; frozen-module workflow tested on a downstream wasi port — .py source compiles cleanly, annotations land in the generated .mpy and round-trip through import.
  • Boards I did NOT test directly: stm32, esp32, esp8266, rp2, samd, mimxrt, renesas-ra, qemu. The change is gated behind a default-off config flag and CI's automated code-size report confirms zero delta on bare-arm / minimal x86 when off.

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 via getattr(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.

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

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.51%. Comparing base (d901e98) to head (2ecbb9e).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown

Code size report:

Reference:  unix/README: Update the supported targets list. [d901e98]
Comparison: py/compile: optionally build __annotations__ on classes (PEP 526) [merge of 2ecbb9e]
  mpy-cross:  +184 +0.048% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:  +184 +0.021% standard
      stm32:  +108 +0.027% PYBV10
      esp32:  +192 +0.011% ESP32_GENERIC[incl +192(data)]
     mimxrt:  +104 +0.026% TEENSY40
        rp2:  +216 +0.023% RPI_PICO_W
       samd:   +80 +0.029% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:   +61 +0.013% VIRT_RV32

@Josverl

Josverl commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Hallo,
Could you please use the (required) PR template - you can update this PR ,no need to close and re-create.

Also I notice a comment in your PR that does not make sense in the context of MicroPython:

typing.get_type_hints, dataclasses, pydantic, attrs, and any library that introspects field types.

these are all CPython references , Not MicroPython. Could it be that this PR has been AI Generated ?

@Josverl Josverl added the needs-info This issue needs more information to be resolvable label Jun 14, 2026
@agatti

agatti commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

They're discarded on purpose, see the discussion in #17643 (starting from comment #17643 (comment)).

@zacharywhitley

Copy link
Copy Markdown
Author

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-int annotations on var: T = const(val) lines (answer: yes, drop the type check; the annotation is then discarded because const() folding doesn't need it).

This PR doesn't touch that path. It changes a separate site in py/compile.c — class-scope annassign emission — which currently parses the type then drops the result on the floor (compile.c literally has // the annotation is in pns1->nodes[0] and is ignored). With MICROPY_PY_BUILTINS_CLASS_ANNOTATIONS=1 the compiler seeds __annotations__ = {} at class body entry and emits a STORE_SUBSCR per annotated member, producing the same cls.__annotations__ dict CPython exposes.

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 dataclasses-style shims that walk __annotations__ to infer field declaration order, rather than the CPython introspection libraries I originally cited). The Generative AI section is filled in too — I did use AI assistance, and I take responsibility for the code and description.

Happy to address other concerns once the code-path distinction is clear.

@Josverl

Josverl commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Thanks using the template.

You state :

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.

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.
While I am a strong supporter of a adding more typing features to MicroPython, the code impact and runtime impact of this partial implementation of PEP 526 does not make sense to me based on what is presented so far.

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

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

Labels

needs-info This issue needs more information to be resolvable

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants