Visitar URL original
Feat/type checking by Josverl · Pull Request #19742 · micropython/micropython · GitHub
Skip to content

Feat/type checking - #19742

Draft
Josverl wants to merge 6 commits into
micropython:masterfrom
Josverl:feat/TYPE_CHECKING
Draft

Josverl wants to merge 6 commits into
micropython:masterfrom
Josverl:feat/TYPE_CHECKING

Conversation

@Josverl

@Josverl Josverl commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Special cases the parser's handling of TYPE_CHECKING.
It treats an assignment of exactly TYPE_CHECKING = False as an private constant that can
be further optimised (out) by the compiler. It is treated similar to _TYPE_CHECKING = const(False).

This allows code that is only needed by static type checkers, such as imports
from typing or method stubs, to be added without any runtime or code size
overhead, because code guarded by if TYPE_CHECKING: is removed from the
bytecode::

TYPE_CHECKING = False
if TYPE_CHECKING:
    from typing import Callable

class Display:
     def write_cmd(self, cmd: int, cb: Callable[[int], None] | None = None) -> None: 
         print("cmd", hex(cmd))
         if cb:
             cb(cmd)

d = Display()
d.write_cmd(0x01) 
d.write_cmd(0x02, lambda x: print("callback", hex(x)))
d.write_cmd(0x03, lambda : print("bad callback")) # <-- Type Check Error:
# Function accepts too few positional parameters; expected 1 but received 0

Static type checkers always treat TYPE_CHECKING as True, so they still
see the guarded code. As with other module-private constants, no
TYPE_CHECKING global variable is created. Only the bare name is recognised:
from typing import TYPE_CHECKING and typing.TYPE_CHECKING are not
optimised.

A .mpy built with the feature contains no qstrs for the guarded names at all.
Importing a .py file saves less heap than importing a pre-compiled .mpy. The lexer still interns
every name in the file, guarded or not, so those qstrs are still allocated; only bytecode and global-dict space is
saved.

closes: #19734

Testing

  • unix port and several other ports were build
  • Added test to verify that the bytecode has been reduced.
  • Tested on Unix port

I created a few sample modules to test the optimisation and to measure the impact.
(Currently these are not part of the PR, but can be shared)

Four sample modules use the pattern. Each was compiled as written ("after") and with
TYPE_CHECKING renamed to TYPE_CHECKINX ("before"). The rename keeps the name length identical
and turns the optimisation off, so "before" matches the old behaviour exactly.

Scenario Contents
s1_min 3 guarded typing names + framebuf function annotated with a Callable
s2_typical 7 typing names + typing_extensions.TypeAlias + a guarded type alias definition
s3_class_stubs 2 guarded @overload stubs + the real method (SSD1306 style)
s4_multi module import guard + class stub guard + function-local guard

Results

.mpy sizes are the flash or filesystem space taken by the deployed module. Heap saved is measured
after import on the 64-bit unix port, in 32-byte GC blocks, so those numbers are coarse.

Sample .mpy before after saved heap saved (.py / .mpy)
s1_min 202 121 81 160 / 160
s2_typical 307 90 217 544 / 672
s3_class_stubs 273 144 129 32 / 32
s4_multi 245 112 133 64 / 192

For comparison, s1_min with the three guard lines deleted compiles to 120 bytes, so the guard so the guard
costs only 1 byte of line-number info.
That growth is possibly caused by included debug line numbers that were pushed into the double-digit range (not verified)

Trade-offs and Alternatives

The trade off is adding code to the firmware , with the intent to reduce the overall firmware size
Additions:

Board (port) Arch text base text patched Δ text Δ data Δ total
RPI_PICO (rp2) Cortex-M0+ 353,924 353,988 +64 0 +64
PYBV11 (stm32) Cortex-M4 378,876 378,992 +116 0 +116
ESP8266_GENERIC (esp8266) Xtensa LX106 639,072 639,144 +72 0 +72
ESP32_GENERIC (esp32) Xtensa LX6 1,429,473 1,429,569 +96 +32 +128
ESP32_GENERIC_C3 (esp32) RISC-V (RV32IMC) 1,424,261 1,424,303 +42 +24 +66
ESP32_GENERIC_C6 (esp32) RISC-V (RV32IMAC) 1,613,785 1,613,827 +42 +24 +66

This is to be offset by the zero-overhead addition of one or more frozen MicroPython modules that can use type annotations.

An initial rough test estimates the saving per mpy-cross compiled module at 64 - 400 bytes for pre-compiled .mpy modules, depending on the use of type annotations.

This would indicate that the feature saves firmware (and memory) if one or more frozen python modules use TYPE_CHECKING guards.

Should this be mpy-cross only ?
It is possible to only include this optimisation in mpy-cross, to avoid any impact on firmware size.

  • In mpconfig.h, default the option to off: #define MICROPY_COMP_TYPE_CHECKING (0).
  • In mpy-cross/mpconfigport.h, turn it on with #define MICROPY_COMP_TYPE_CHECKING (1), next to the other MICROPY_COMP_* settings that mpy-cross already sets.
  • In CI, turn it on in the unix coverage variant (mpconfigvariant.h), so the tests keep exercising the parser code.

Benefits of mpy-cross only:

  • Firmware size impact would then be zero on every port. Device firmware never contains the extra parser code.
  • Frozen modules: these are compiled by mpy-cross, so the savings apply to firmware images. This is probably the biggest win (micropython-lib, drivers).
  • .mpy files deployed to a device, for example via mip or mpremote : same Bytes saved per .mpy module as in the report above.

Against mpy-cross only:

  • .py files compiled on the device keep the guarded code. They still run correctly, because TYPE_CHECKING is a normal global set to False, but they don't get the size or RAM savings.
  • The same source behaves slightly differently depending on how it's compiled, rather rare, but would need some documentation.

Generative AI

I did not use generative AI tools when creating this PR.

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.

Josverl and others added 6 commits September 12, 2026 16:04
Used in Build:
tools/ci.sh
tools/boardgen.py
tools/makemanifest.py
tools/manifestfile.py
tools/mpy-tool.py
tools/insert-usb-ids.py
tools/mpy_ld.py
tools/uf2conv.py
tools/uf2families.json
tools/file2h.py
tools/ar_util.py
tools/dfu.py

Used in Tests:
tools/pyboard.py

Used in build docs:
tools/gen-cpydiff.py

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
This uses the YAML anchors and aliases feature to re-use the
same set of paths for both the push and pull_request triggers.

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Treat TYPE_CHECKING as a MicroPython const() to allow the code folding
mechanism to remove code guarded by ``if TYPE_CHECKING:``
from the bytecode.

Signed-off-by: Jos Verlinde <jos_verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <jos_verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <jos_verlinde@hotmail.com>
@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.55%. Comparing base (52b5fbc) to head (129289f).
⚠️ Report is 39 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #19742   +/-   ##
=======================================
  Coverage   98.55%   98.55%           
=======================================
  Files         182      182           
  Lines       23335    23354   +19     
  Branches        5        5           
=======================================
+ Hits        22998    23017   +19     
  Misses        336      336           
  Partials        1        1           
Flag Coverage Δ
unix-coverage-32bit 98.55% <100.00%> (+<0.01%) ⬆️
unix-coverage-64bit 98.52% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Code size report:

Reference:  tests/multi_net: Accept OSError for TLS handshake failures. [3c01f4a]
Comparison: tests/type_checking: Test TYPE_CHECKING folding. [merge of 129289f]
  mpy-cross:   +88 +0.023% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:  +120 +0.014% standard
      stm32:   +56 +0.014% PYBV10
      esp32:  +128 +0.007% ESP32_GENERIC[incl +32(data)]
     mimxrt:   +72 +0.018% TEENSY40
        rp2:   +80 +0.008% RPI_PICO_W
       samd:   +40 +0.014% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:   +61 +0.013% VIRT_RV32

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.

Treat TYPE_CHECKING = False as an internal const value

1 participant