Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 15 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: Three emitters compile the same function three ways · Viper integers are machine words · Check yourself. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

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: raise needs 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:

Interactive · answer and compare Which emitter should a hot function use? Answer three questions about the function and the builds it must run on; the table marks what fits and why.
What does the hot function mostly do?
Can its integers ever need more than 31 bits?
Must the same file also run on a build without an emitter, such as unix on an Apple Silicon Mac?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Bytecode, the native emitter and the viper emitter compared for one hot function
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

  1. Question 1 of 2 What changes when a working function gets @micropython.native?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It runs as machine code with the same behaviour, roughly twice as fast and larger

    The native emitter turns the function into machine code that performs the same operations on Python objects as the bytecode did. It behaves the same, runs roughly twice as fast, and takes more room.

  2. Question 2 of 2 A module with one @micropython.viper function is imported on the unix port on an Apple Silicon Mac. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The module fails to compile: SyntaxError, invalid micropython decorator

    Decorators are handled by the compiler, and a build without an emitter for its processor raises SyntaxError for native and viper alike, so no part of the module is imported.

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:

The same operations in Python, on a 32-bit board and on a 64-bit build Python · words/viper_words.py
"""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

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.

Programmer Calculator (Bitwise) See the 32 bits of a value and its signed and unsigned readings.

Checkpoint

Check: Viper's machine-word integers

  1. Question 1 of 2 A viper function on a Pico returns a * b for its int arguments a and b. What does it return for a = b = 70,000? (In Python the product is 4,900,000,000.)

    Type a number.

    A hint

    Subtract 2³² = 4,294,967,296 once, then check the sign of what is left.

    Show the answer: Question 1 of 2

    Answer: 605032704

    4,900,000,000 − 4,294,967,296 = 605,032,704, which is below 2³¹, so the signed 32-bit word reads it as a positive 605,032,704. A 64-bit build returns 4,900,000,000.

  2. Question 2 of 2 A viper function on a Pico computes the word 0xFFFFFFFF and returns it. What does Python see with -> uint, and with -> int?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 4294967295 with uint, -1 with int

    The 32 bits are the same. Declared -> uint, the result is read as 0 to 2³² − 1, so 4,294,967,295; declared -> int, as a signed word, so −1.

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.

  1. Question 1 of 6 Does a board need import micropython for @micropython.viper to work?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: No; the compiler recognises the decorator itself

    MicroPython's compiler reads @micropython.native and @micropython.viper as instructions to itself. A file that must also run on CPython still imports micropython, to have something to decorate with there.

  2. Question 2 of 6 Which of these does the speed guide list as limits of viper functions?

    Choose every answer that is right.

    Show the answer: Question 2 of 6

    Answer:

    • raise must be given an argument
    • No default argument values
    • Floating point is allowed but not optimised

    Viper inherits the native emitter's limits, such as raise with an argument, and adds its own: no default argument values and no optimisation of floats.

  3. Question 3 of 6 A hot function formats log lines from a dict of readings. Which emitter is worth trying first?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: Native, since the work is on Python objects that viper does not speed up

    Native code keeps every Python operation and removes the bytecode interpreter's overhead. Viper's extra speed is for machine-word integers and pointers, which string and dict code does not use.

  4. Question 4 of 6 A viper function on a Pico, declared -> int, returns 1 << n for its int argument n. What does it return for n = 31?

    Type a number.

    Show the answer: Question 4 of 6

    Answer: -2147483648

    Shifting 1 left by 31 sets the top bit of a 32-bit word, which a signed word reads as −2³¹ = −2,147,483,648. A 64-bit build returns 2,147,483,648.

  5. Question 5 of 6 A viper function sums bytes of 255 into an int on a Pico. After how many bytes does the sum first pass the largest signed 32-bit value, 2,147,483,647?

    Type a number.

    A hint

    Divide 2,147,483,647 by 255 and take the next whole number.

    Show the answer: Question 5 of 6

    Answer: 8421505 bytes

    2,147,483,647 / 255 = 8,421,504.5, so 8,421,504 bytes still fit, at 2,147,483,520, and the 8,421,505th byte takes the sum past the limit, where it wraps to a negative number.

  6. Question 6 of 6 Your viper code passes its tests on the unix port of an x86-64 Linux CI runner. What can still go wrong on a Pico?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: Any value between 2³¹ and 2⁶³ wraps on the Pico but not in CI

    Viper integers are machine words, 64 bits on x64 and 32 on the RP2040. Tests on x64 must also check that values stay within 32 bits, or run on a 32-bit build.

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.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.native keeps Python’s behaviour and runs roughly twice as fast; @micropython.viper adds 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.