MicroPython for Microcontrollers Module 7 – Memory limits, speed, native and viper code
@micropython.native and @micropython.viper
What the native and viper emitters of MicroPython compile, what each gains over bytecode, viper's machine-word integers and pointers, and testing them.
What you will learn
- Choose between bytecode, native and viper for a hot function and the builds it must run on
- Predict viper's machine-word results on a 32-bit board and on a 64-bit build
- Use viper's int, uint and pointer types, with casts at the start of the function
- Test viper code on a computer with a stand-in micropython module, overflow included
Before you start
What you’ll learn · Before you start
What you will learn
- Choose between bytecode, native and viper for a hot function and the builds it must run on
- Predict viper's machine-word results on a 32-bit board and on a 64-bit build
- Use viper's int, uint and pointer types, with casts at the start of the function
- Test viper code on a computer with a stand-in micropython module, overflow included
Before you start
On this page
A viper function on a Pico that multiplies its two int arguments returns 1,410,065,408 for 100,000 × 100,000. Its
integers are 32-bit machine words, so the arithmetic wraps modulo 2³², the price viper pays for running integer and
buffer code much faster than bytecode.
The native and viper emitters compile a Python function into the processor’s own instructions. Below: what each does, which builds have it, how viper’s words and pointers behave, and how to test viper code on a computer.
Three emitters compile the same function three ways
By default MicroPython compiles a function to bytecode, which its virtual machine runs one instruction at a time. Two decorators choose another emitter for one function, @micropython.native and @micropython.viper, and the compiler reads them itself, so they need no import:
@micropython.native: the native emitter: The part of MicroPython's compiler that turns a function decorated with @micropython.native into the processor's own instructions. The code does the same object operations as bytecode, so it behaves the same and runs roughly twice as fast. writes machine code that does the same object operations as the bytecode would. MicroPython’s speed guide puts it at roughly twice as fast as bytecode, with larger code and a few limits:raiseneeds an argument, and scheduled callbacks do not run while native code runs.@micropython.viper: the viper emitter: The compiler's second machine-code emitter, used for @micropython.viper functions. It adds types of its own, machine-word integers and pointers into buffers, and runs integer and buffer code much faster than bytecode, at the cost of Python's exact integers. adds types of its own, machine-word integers and pointers into buffers, and optimises further. The guide calls it not fully compliant: it gains most on integer maths and bit operations, takes no default argument values and does not optimise floats.
A native function behaves as before, within those limits, only faster; a viper function may give different integer
results, as the next section shows. Each build has the emitter of its own processor or none: x86 and x64, 32-bit ARM
including the Thumb code of the RP2040, Xtensa on the ESP32, and since 1.24 RISC-V on chips such as the ESP32-C3. The unix port has no emitter for 64-bit ARM, so on an Apple Silicon Mac or
a 64-bit Raspberry Pi system both decorators stop the module at compile time with
SyntaxError: invalid micropython decorator. Answer for a hot function of your own:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Emitter | Speed | Python semantics | Code size | Builds that run it |
|---|---|---|---|---|
| Bytecode Written as: a plain Python function | Mixed the baseline the others are measured against | Good all of MicroPython's Python | Good the most compact form of the three | Good every build of every port |
| Native Written as: @micropython.native on the function | Mixed roughly twice as fast as bytecode, the speed guide says | Good the same object operations as bytecode, with a few limits | Mixed larger than bytecode: it is machine code | Mixed only builds with an emitter for their processor |
| Viper Written as: @micropython.viper, with viper types | Good much faster for integer and buffer code | Poor integers are machine words that wrap; not fully compliant | Mixed machine code, larger than bytecode | Mixed only builds with an emitter for their processor |
How the answers decide
- “What does the hot function mostly do?” Calls, strings, lists and dicts: adds a caveat to Viper. Viper's gains come from machine-word maths and pointers, as the speed guide says; code that works on Python objects does not use them.
- “Can its integers ever need more than 31 bits?” Yes, or I am not sure: rules out Viper. Viper's integers wrap at the machine word, so a value that outgrows it silently becomes a different number.
- “Must the same file also run on a build without an emitter, such as unix on an Apple Silicon Mac?” Yes: rules out Native and Viper. A build without an emitter refuses the decorator at compile time, so the whole module fails to import there.
Checkpoint
Check: Bytecode, native and viper
Viper integers are machine words
A viper int is a machine word: The number of bits a processor handles in one step: 32 on the RP2040 and the ESP32, 64 on an x86-64 computer. Viper's integers are machine words, so their arithmetic wraps when a result needs more bits.: 32 bits on rp2 and esp32, 64 bits on an x64 build.
The speed guide is explicit: on 32-bit hardware, viper’s arithmetic wraps modulo 2³². This program works out what
each build returns for the same operations on a viper function’s int arguments, with the board’s word kept by
masking:
"""Viper's integers are machine words: what Python, a 32-bit board and a 64-bit build give for the same operations.
The operands are a viper function's int arguments, so the board computes on them at run time.
Written as literals, an expression such as 100000 * 100000 is worked out by the compiler first.
"""
def word(value, bits):
"""value as a signed machine word of this many bits, the way viper's int keeps it."""
value &= (1 << bits) - 1
return value - (1 << bits) if value >> (bits - 1) else value
def uword(value, bits):
"""value as an unsigned machine word: viper's uint, seen by Python on return."""
return value & ((1 << bits) - 1)
CASES = [
("a << b, a = 1, b = 31", 1 << 31, word),
("a * b, a = b = 100000", 100000 * 100000, word),
("a + 1, a = 2**31 - 1", (1 << 31) - 1 + 1, word),
("-1, returned as int", -1, word),
("-1, returned as uint", -1, uword),
]
print("%-22s %20s %13s %20s" % ("operation", "Python", "rp2, esp32", "unix x64"))
for text, exact, kind in CASES:
print("%-22s %20d %13d %20d" % (text, exact, kind(exact, 32), kind(exact, 64))) Output
operation Python rp2, esp32 unix x64 a << b, a = 1, b = 31 2147483648 -2147483648 2147483648 a * b, a = b = 100000 10000000000 1410065408 10000000000 a + 1, a = 2**31 - 1 2147483648 -2147483648 2147483648 -1, returned as int -1 -1 -1 -1, returned as uint -1 4294967295 18446744073709551615
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 viper_words.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Shifting 1 left by 31 sets the top bit of a 32-bit word, which a signed word reads as −2,147,483,648, and 100,000 ×
100,000 keeps only its low 32 bits. The 64-bit build keeps the products and sums exact, which is why code tested on a
computer’s unix build can still be wrong on a Pico. Literals would not show it: the compiler works out
100000 * 100000 itself, before the function runs, so the values come in as arguments. The return type decides how
the caller sees the word: -1 returned as int stays −1, while a function declared -> uint returns 4,294,967,295
on the board.
Viper’s other types are named for what they point at. ptr8, ptr16 and ptr32 read and write 8, 16 and 32-bit
values in a buffer such as a bytearray, by index; ptr points to objects; and the casts int(), uint() and
bool() turn a Python object into a native value. No pointer checks its index against the buffer’s length.
Checkpoint
Check: Viper's machine-word integers
Check yourself
Bytecode, native and viper, one question at a time.
Practice that adapts to you
Practise this lesson
6 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Viper's types and pointers, tested on a computer before the board Pro: opens with a Pro, Premium or Ultimate pass
Viper code can be tested on a computer long before a board is plugged in, if the test supplies what the board’s compiler would. This part gives the rules of viper’s types, a stand-in micropython module, and the one check a computer cannot do by itself.
In this part:
- Casts, pointers and uint, rule by rule
- A stand-in micropython module for host tests
- Keeping viper values inside the word
1 runnable example (Python, fake_micropython.py, sums.py) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Where viper's machine words bite: constants, comparisons and code size Pro: opens with a Pro, Premium or Ultimate pass
Some viper code compiles on one build and is refused on another, and some runs but quietly costs more than it saves. This part reproduces the compile-time errors with the typing rules of the emitter, and adds up what machine code costs.
In this part:
- Big constants are objects, and objects do not mix with ints
- What machine code costs
- Exercise: a viper Fletcher-16 checksum
- Write a viper Fletcher-16 checksum
- Results of the sample tests
1 runnable example (Python) · 1 exercise with sample tests · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
Warm-up (fresher): What is the difference between @micropython.native and @micropython.viper?
Both compile a function to machine code instead of bytecode. Native code does the same object operations as bytecode, so the function behaves as before and runs roughly twice as fast. Viper adds its own types, machine-word integers and pointers into buffers, and runs integer and buffer code much faster, but its integers wrap at the machine word, so it is not fully compatible with Python.
-
Mid to senior
A viper checksum over a file's bytes matches the CPython reference in every unit test, yet a Pico reports a different value for large files. What is the likely cause, how do you confirm it, and how do you make the tests catch it?
-
Mid to senior
Your CI runs MicroPython's unix port on an Apple Silicon runner, and every module with a viper function fails at import with SyntaxError: invalid micropython decorator. What is going on, and how do you test viper code in CI?
-
Senior to staff
A Pico must checksum each 64 KB block it reads from flash within 20 ms, and the bytecode version takes 60 ms. How do you choose between native, viper and the inline assembler, and what evidence do you collect before you decide?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does @micropython.native do to a function?
It compiles it to machine code that does the same object operations as bytecode: roughly twice as fast, with larger code; raise needs an argument, and scheduled callbacks do not run while the code runs.
-
What does @micropython.viper add, and at what cost?
Machine-word integers and pointers into buffers, so integer and bit operations run much faster. It is not fully compliant: it takes no default argument values, does not optimise floats, and its integers wrap.
-
What does a viper function on a Pico return for 100,000 × 100,000 passed in as int arguments?
1,410,065,408: viper integers are 32-bit machine words on rp2 and esp32, so the product wraps modulo 2³². A 64-bit build keeps it exact.
-
Why do @micropython.native and @micropython.viper fail on the unix port of an Apple Silicon Mac?
The unix port has no emitter for 64-bit ARM, so both decorators stop the module at compile time with SyntaxError: invalid micropython decorator.
-
What are ptr8, ptr16 and ptr32 in viper code?
Pointers that read and write 8, 16 and 32-bit values in a buffer such as a bytearray, by index. None of them checks the index against the buffer's length.
-
What does a viper function declared -> uint return for −1 on a board?
4,294,967,295: the return type decides how the caller reads the word. Returned as int, −1 stays −1.
Key takeaways
@micropython.nativekeeps Python’s behaviour and runs roughly twice as fast;@micropython.viperadds machine-word types and pointers and runs integer code much faster, at the cost of exact integers.- Each build has the emitter of its own processor or none, and a build without one refuses the decorators at compile time.
- Viper integers wrap at the machine word, 32 bits on rp2 and esp32, so code that is right on a 64-bit build can be wrong on a Pico.
Terms in this lesson
- machine word
- The number of bits a processor handles in one step: 32 on the RP2040 and the ESP32, 64 on an x86-64 computer. Viper's integers are machine words, so their arithmetic wraps when a result needs more bits.
- native emitter
- The part of MicroPython's compiler that turns a function decorated with @micropython.native into the processor's own instructions. The code does the same object operations as bytecode, so it behaves the same and runs roughly twice as fast.
- viper emitter
- The compiler's second machine-code emitter, used for @micropython.viper functions. It adds types of its own, machine-word integers and pointers into buffers, and runs integer and buffer code much faster than bytecode, at the cost of Python's exact integers.
References
- Maximising MicroPython speed (the native and viper code emitters) (MicroPython project)
- MicroPython .mpy files (native code and its architecture) (MicroPython project)
- py/compile.c, the micropython decorators (v1.29.0) (MicroPython project)
- py/emitnative.c, viper types and ViperTypeError (v1.29.0) (MicroPython project)
- py/nativeglue.c, casts from Python objects (v1.29.0) (MicroPython project)
- py/smallint.h, the range of small integers (v1.29.0) (MicroPython project)
- ports/unix/mpconfigport.h, the emitter each processor gets (v1.29.0) (MicroPython project)
- ports/rp2/mpconfigport.h, Thumb and RISC-V emitters (v1.29.0) (MicroPython project)
- ports/esp32/mpconfigport.h, Xtensa and RISC-V emitters (v1.29.0) (MicroPython project)
- MicroPython v1.24.0 release notes (the RISC-V native emitter) (MicroPython project)
- MicroPython v1.26.0 release notes (native and viper code generation, long jumps on Thumb v1) (MicroPython project)
- RFC 1146: TCP Alternate Checksum Options (Fletcher's checksum) (IETF)
- memoryview: cast() (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress