MicroPython Data Types and Variables
Data Types Overview
MicroPython supports the principal Python 3 data types used in embedded applications. Variables refer to objects rather than having fixed declared types. Memory usage, integer range and floating-point precision depend on the MicroPython firmware build.
| Type | Example | Mutable | Typical use |
|---|---|---|---|
| int | count = 100 | No | Counters and bit masks |
| float | voltage = 3.3 | No | Sensor measurements |
| bool | enabled = True | No | Status flags |
| str | name = “Sensor” | No | Messages and labels |
| bytes | packet = b”\x01\x02″ | No | Fixed binary packets |
| bytearray | buffer = bytearray(16) | Yes | Reusable I/O buffers |
| list | samples = [1, 2, 3] | Yes | Sample collections |
| tuple | point = (10, 20) | No | Fixed groups |
| dict | config = {“rate”: 100} | Yes | Configuration |
| set | pins = {1, 2, 3} | Yes | Unique values |
| NoneType | result = None | No | Unavailable result |
Variables and Assignment
Assign a value to create or rebind a variable:
temperature = 24.5 sample_count = 0 sensor_enabled = True device_name = "Environmental Monitor"
A name may subsequently refer to another type, although consistent types make firmware easier to maintain.
reading = 100 reading = 23.75 reading = "Unavailable"
Variable names are case-sensitive, cannot begin with a digit and cannot be Python keywords. Prefer sample_count to names that shadow built-ins such as list or int.
Multiple assignment and unpacking:
x, y, z = 10, 20, 30 minimum = maximum = 0 minimum, maximum = maximum, minimum
Integers
Integers represent whole numbers and are useful for register values, counts and binary protocol fields.
counter = 125 offset = -20 binary_mask = 0b10101010 hex_value = 0xA5 octal_value = 0o17
| Notation | Example | Decimal value |
|---|---|---|
| Decimal | 255 | 255 |
| Binary | 0b11111111 | 255 |
| Hexadecimal | 0xFF | 255 |
| Octal | 0o377 | 255 |
| Operator | Example | Result |
|---|---|---|
| Addition | 17 + 5 | 22 |
| Subtraction | 17 – 5 | 12 |
| Multiplication | 17 * 5 | 85 |
| True division | 17 / 5 | 3.4 |
| Floor division | 17 // 5 | 3 |
| Remainder | 17 % 5 | 2 |
| Power | 17 ** 2 | 289 |
Floor division rounds towards negative infinity:
print(-7 // 3) # -3 print(-7 % 3) # 2
MicroPython integer range and long-integer support depend on the firmware build. Do not assume either a fixed 32-bit limit or unlimited precision.
Extract bytes from an integer:
value = 0x1234 low_byte = value & 0xFF high_byte = (value >> 8) & 0xFF print(hex(low_byte)) # 0x34 print(hex(high_byte)) # 0x12
Floating-Point Numbers
voltage = 3.3 current = 0.125 temperature = -12.75 frequency = 1.5e6
| Notation | Value |
|---|---|
| 1.5e6 | 1,500,000 |
| 2.2e-3 | 0.0022 |
| 4.7e-6 | 0.0000047 |
MicroPython builds may use IEEE 754 single-precision or double-precision floats. Single precision offers approximately 6–7 significant decimal digits; double precision approximately 15–16. Binary floating-point cannot represent every decimal fraction exactly.
Use tolerances rather than direct equality for measured values:
measured = 3.299
target = 3.3
tolerance = 0.01
if abs(measured - target) <= tolerance:
print("Within tolerance")
An approximate conversion from a scaled 16-bit ADC reading to voltage, assuming a linear 0–3.3 V range:
raw = 32768 reference_voltage = 3.3 voltage = raw * reference_voltage / 65535 print(voltage)
The actual ADC voltage relationship depends on the board, calibration, attenuation and reference.
Boolean Values and Truth Testing
bool has the values True and False.
sensor_ready = True
alarm_active = False
if sensor_ready and not alarm_active:
print("Measurement permitted")
| Operator | Meaning |
|---|---|
| and | Both operands truthy |
| or | At least one operand truthy |
| not | Logical negation |
| ==, != | Equality and inequality |
| <, >, <=, >= | Ordering comparisons |
False, None, numeric zero and empty strings or collections are false in a condition. A sensor reading of zero can nevertheless be valid.
reading = 0
if reading is not None:
print("Valid reading:", reading)
Use is None for an unavailable value rather than testing whether the reading is truthy.
Strings
Strings hold Unicode text and are immutable.
device_name = "Pico Sensor" status = 'Ready' message = "Temperature: 25 C"
| Operation | Example | Result |
|---|---|---|
| Length | len(“ESP32”) | 5 |
| First character | “ESP32″[0] | “E” |
| Slice | “ESP32″[0:3] | “ESP” |
| Concatenate | “GPIO” + “15” | “GPIO15” |
| Uppercase | “ready”.upper() | “READY” |
| Split | “10,20”.split(“,”) | [“10”, “20”] |
| Strip whitespace | ” OK \n”.strip() | “OK” |
String formatting, with each statement on a separate line:
temperature = 24.75
message = "Temperature: {:.1f} C".format(temperature)
print(message)
Expected output:
Temperature: 24.8 C
Modern MicroPython builds also support f-strings:
count = 25
print(f"Samples: {count}")
Repeated string concatenation allocates new objects and may increase garbage-collection activity.
Bytes and Bytearrays
Use bytes for immutable binary data and bytearray for mutable buffers.
command = b"\x01\x02\xA5" print(len(command)) # 3 print(command[0]) # 1
buffer = bytearray(4) buffer[0] = 0x10 buffer[1] = 0x20 buffer[2] = 0x30 buffer[3] = 0x40
Byte elements are integers from 0 to 255. Assigning outside that range raises an error.
Convert a 16-bit integer to big-endian bytes and back on firmware supporting these methods:
value = 0x1234 data = value.to_bytes(2, "big") restored = int.from_bytes(data, "big") print(data) # b'\x12\x34' print(restored) # 4660
For repeated peripheral reads, use a preallocated buffer where the API supports it:
from machine import I2C, Pin i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=100000) buffer = bytearray(8) i2c.readfrom_into(0x48, buffer)
The I2C example uses Raspberry Pi Pico GP1 and GP0. Address 0x48, read length and pull-up resistors must suit the connected device.
Lists
Lists are ordered and mutable.
samples = [120, 125, 118, 130] print(samples[0]) print(samples[-1]) samples[1] = 126 samples.append(135)
| Operation | Expression |
|---|---|
| Length | len(samples) |
| Append | samples.append(135) |
| Remove last item | samples.pop() |
| Slice | samples[1:3] |
| Sum | sum(samples) |
| Minimum | min(samples) |
| Maximum | max(samples) |
Calculate a sample average, guarding against an empty list:
samples = [1020, 1018, 1025, 1022]
if samples:
average = sum(samples) / len(samples)
print(average)
An unbounded list can exhaust a small heap. A circular buffer maintains fixed storage:
samples = [0] * 8
index = 0
def add_sample(value):
global index
samples[index] = value
index = (index + 1) % len(samples)
for value in range(12):
add_sample(value)
print(samples)
The resulting storage order is [8, 9, 10, 11, 4, 5, 6, 7]. Track the insertion index when reading the samples chronologically.
Tuples
Tuples are ordered and immutable. They are useful for fixed groups of values and multiple return values.
coordinates = (10, 20) x, y = coordinates print(x) print(y)
def measurement():
temperature = 24.5
humidity = 55.0
return temperature, humidity
temp, humidity = measurement()
print(temp, humidity)
A tuple itself is immutable, but a mutable object contained within a tuple can still be modified.
Dictionaries
Dictionaries map keys to values.
configuration = {
"sample_interval": 1000,
"threshold": 30.0,
"enabled": True
}
interval = configuration["sample_interval"]
configuration["threshold"] = 32.0
configuration["mode"] = "automatic"
enabled = configuration.get("enabled", False)
| Operation | Expression |
|---|---|
| Lookup | configuration[“threshold”] |
| Lookup with default | configuration.get(“mode”, “manual”) |
| Add or replace | configuration[“enabled”] = False |
| Remove | configuration.pop(“mode”) |
| Iterate keys | configuration.keys() |
| Iterate values | configuration.values() |
| Iterate pairs | configuration.items() |
Accessing a missing key with square brackets raises KeyError. Use .get() when a default is appropriate. Dictionaries offer readable configuration storage but consume more memory than simple scalar variables.
Sets
Sets contain unique, hashable values. Their iteration order is not a positional sequence.
active_channels = {1, 2, 3}
active_channels.add(4)
active_channels.discard(2)
a = {1, 2, 3}
b = {3, 4, 5}
common = a & b
combined = a | b
difference = a - b
symmetric = a ^ b
| Operator | Meaning |
|---|---|
| & | Intersection |
| \| | Union |
| – | Difference |
| ^ | Symmetric difference |
Sets are useful for membership checks and removing duplicates, but can have significant memory overhead for small collections.
None and Missing Measurements
Use None to represent the absence of a value rather than using zero when zero could be valid.
def get_valid_reading(raw):
if raw < 0:
return None
return raw
reading = get_valid_reading(-1)
if reading is None:
print("Invalid reading")
else:
print(reading)
Prefer is None and is not None for identity checks.
Type Inspection and Conversion
value = 100 print(type(value)) print(isinstance(value, int))
| Conversion | Example | Result |
|---|---|---|
| Integer | int(“123”) | 123 |
| Float | float(“3.5”) | 3.5 |
| String | str(123) | “123” |
| Boolean | bool(0) | False |
| List | list((1, 2)) | [1, 2] |
| Tuple | tuple([1, 2]) | (1, 2) |
| Bytes | bytes([1, 2]) | b”\x01\x02″ |
| Bytearray | bytearray(4) | Four zero bytes |
Validate text received from a serial port or network before using it numerically:
received = "125"
try:
value = int(received)
except ValueError:
value = None
Variable Scope and Object References
Assignments inside functions normally create local bindings.
def calculate_total():
count = 10
return count * 2
print(calculate_total())
Use global when a function needs to rebind a module-level name:
sample_count = 0
def increment_count():
global sample_count
sample_count += 1
increment_count()
print(sample_count)
Mutating an existing global list does not rebind the name:
samples = []
def add_sample(value):
samples.append(value)
add_sample(25)
print(samples)
Assignment shares an object reference rather than copying the object:
a = [1, 2, 3] b = a b[0] = 99 print(a) # [99, 2, 3]
Create a shallow copy when separate list storage is required:
a = [1, 2, 3] b = a.copy() b[0] = 99 print(a) # [1, 2, 3] print(b) # [99, 2, 3]
== compares values; is compares object identity. Do not use is as a general replacement for equality.
Constants
Uppercase names conventionally identify values that should not change:
MAX_SAMPLES = 100 ADC_REFERENCE = 3.3
MicroPython offers an integer constant optimisation:
from micropython import const LED_PIN = const(15) BUFFER_SIZE = const(128) STATUS_MASK = const(0x80)
const() does not make arbitrary Python objects immutable. Optimisation behaviour depends on the firmware and use of the constant.
Bitwise Operations
| Operator | Purpose | Example |
|---|---|---|
| & | AND | value & 0x0F |
| \| | OR | value \| 0x80 |
| ^ | XOR | value ^ 0x01 |
| ~ | NOT | ~value |
| << | Left shift | value << 2 |
| >> | Right shift | value >> 2 |
Set, clear, toggle and test bit 3:
register = 0
register |= (1 << 3) # Set
register &= ~(1 << 3) # Clear
register ^= (1 << 3) # Toggle
if register & (1 << 3):
print("Bit 3 is set")
Extract a three-bit field beginning at bit 2:
register = 0b10110110 field = (register >> 2) & 0b111 print(field) # 5
Apply masks when fixed-width unsigned behaviour is required. Python integers are not inherently limited to 8 or 16 bits.
Memory-Efficient Buffers
A memoryview accesses existing buffer storage without copying the underlying bytes:
buffer = bytearray(16) header = memoryview(buffer)[:4] header[0] = 0xAA header[1] = 0x55 print(buffer[0]) # 170 print(buffer[1]) # 85
Creating the view itself uses some memory, but the underlying bytes are shared.
Inspect the MicroPython managed heap:
import gc
gc.collect()
print("Free heap:", gc.mem_free())
print("Allocated:", gc.mem_alloc())
These values do not necessarily describe all physical RAM on the board.
Common Errors and Diagnostics
| Symptom | Likely cause | Corrective action |
|---|---|---|
| TypeError | Incompatible operand types | Convert explicitly |
| ValueError | Invalid numeric string or value | Validate input |
| IndexError | Collection index out of range | Check bounds |
| KeyError | Missing dictionary key | Use .get() or test membership |
| MemoryError | Heap exhausted | Reuse buffers; bound collections |
| ZeroDivisionError | Divisor is zero | Validate before division |
| OverflowError | Numeric operation exceeds supported range | Check build and numeric representation |
| Unexpected float comparison | Rounding or precision | Compare with tolerance |
| Unexpected list modification | Shared mutable reference | Copy where appropriate |
| UnboundLocalError | Local name referenced before assignment | Review scope and global |
| Cannot modify bytes | Immutable object | Use bytearray |
| Invalid byte assignment | Value outside 0–255 | Validate or mask |
| Zero reading ignored | Truth-value check | Check explicitly against None |
Quick Reference
| Task | Expression |
|---|---|
| Inspect type | type(value) |
| Test type | isinstance(value, int) |
| Convert to integer | int(value) |
| Convert to float | float(value) |
| Convert to string | str(value) |
| Get length | len(collection) |
| Fixed-size list | [0] * 16 |
| Mutable byte buffer | bytearray(16) |
| Last list element | items[-1] |
| Shallow list copy | items.copy() |
| Add list element | items.append(value) |
| Safe dictionary lookup | config.get(“key”, default) |
| Test missing result | value is None |
| Low byte | value & 0xFF |
| High byte | (value >> 8) & 0xFF |
| Set bit 3 | value \|= (1 << 3) |
| Clear bit 3 | value &= ~(1 << 3) |
| Toggle bit 3 | value ^= (1 << 3) |
| Test bit 3 | bool(value & (1 << 3)) |
| Free heap | gc.mem_free() |
| Garbage collection | gc.collect() |
Practical Data Handling Checklist
- Use integers for counters, register fields and bit masks.
- Confirm float precision before relying on fine calibration calculations.
- Use bytearray for reusable binary I/O buffers.
- Bound the size of lists in continuously running applications.
- Distinguish zero measurements from unavailable measurements.
- Validate serial and network input before converting it.
- Use explicit masks for fixed-width protocol fields.
- Account for shared references when modifying collections.
- Avoid allocations inside hard interrupt callbacks.
- Check managed heap usage under realistic operating conditions.
- Verify firmware support for less common methods.
- Test memory behaviour over extended operation.