The Verdict: Running Python on Arduino Hardware
If you want to run "Arduino in Python" natively on the board itself, you must use the Arduino Nano ESP32 (ABX00092) flashed with MicroPython. Standard AVR-based Arduinos (like the Uno R3 or Nano v3) lack the RAM and 32-bit architecture required to host a Python interpreter; they can only be controlled by Python running on a host PC via pyserial. The Nano ESP32, powered by the ESP32-S3 chip with 8MB of PSRAM, is currently the best official Arduino hardware for native MicroPython development in 2026.
Estimated Time: 45 minutes
Core Concepts: MicroPython REPL, I2C bus initialization, ESP32-S3 GPIO mapping, hardware exception handling.
Parts List and Pin Mapping
This build assumes a 3.3V logic environment. The ESP32-S3 is strictly a 3.3V device. Feeding 5V into the I2C data lines will permanently damage the silicon.
| Component | Exact Model / Variant | Est. Price | Notes |
|---|---|---|---|
| Microcontroller | Arduino Nano ESP32 (ABX00092) | $21.00 | Ensure it is the ESP32 variant, not the classic Nano. |
| Sensor | Adafruit BME280 I2C (PID 2652) | $15.00 | Includes onboard 3.3V regulator and I2C pull-ups. |
| Wiring | 22 AWG Solid Core Jumper Wires | $5.00 | 4 wires required for I2C. |
| Software | Thonny IDE (v4.1+) | Free | Native MicroPython support and file management. |
Pin Mapping Table
The Arduino Nano ESP32 silkscreen labels (A4, A5) map to specific ESP32-S3 GPIO numbers under the hood. MicroPython requires the raw GPIO numbers.
| Nano ESP32 Pin | ESP32-S3 GPIO | BME280 Pin | Function |
|---|---|---|---|
| 3V3 | N/A | VIN | Power (3.3V) |
| GND | N/A | GND | Common Ground |
| A4 | GPIO 25 | SDI | I2C Data (SDA) |
| A5 | GPIO 33 | SCK | I2C Clock (SCL) |
Wiring and MicroPython Flashing Steps
Before writing code, you must flash the MicroPython firmware onto the ESP32-S3. Arduino ships these boards with their own bootloader, so we need to overwrite it with the official MicroPython binary.
- Download the Firmware: Go to the official MicroPython download page and grab the latest stable
.binrelease for the ARDUINO_NANO_ESP32. - Enter ROM Bootloader Mode: Hold the B0 button on the Nano ESP32, tap the **RST** button, then release B0. The board will now appear as a generic USB serial device (e.g.,
COM3on Windows or/dev/cu.usbmodem*on macOS). - Erase and Flash: Open your terminal and use
esptool(install viapip install esptool):esptool --chip esp32s3 --port COM3 erase_flash esptool --chip esp32s3 --port COM3 --baud 460800 write_flash -z 0x0 ARDUINO_NANO_ESP32-20240105-v1.22.2.bin - Wire the I2C Bus: Connect the 4 pins as defined in the mapping table above. Double-check that the BME280 VIN is connected to 3V3, not 5V.
- Connect Thonny: Open Thonny IDE, go to Tools > Options > Interpreter, select
MicroPython (ESP32), and choose your COM port. Press the RST button on the board. You should see the MicroPython REPL prompt (>>>) in the shell.
The Code: I2C Bus Scan and Sensor Verification
This script initializes the I2C bus using explicit GPIO definitions, scans for connected devices, and attempts to read the WHO_AM_I register of the BME280. It includes robust error handling to catch hardware-level I2C faults.
import machine
import time
# Explicit Pin definitions for Arduino Nano ESP32 (ABX00092)
# A4 maps to GPIO25 (SDA), A5 maps to GPIO33 (SCL)
SDA_PIN = 25
SCL_PIN = 33
I2C_FREQ = 400000 # 400kHz Fast Mode
BME280_ADDR = 0x76 # Default for Adafruit breakout (0x77 for some generic clones)
def init_i2c():
"""Initialize I2C bus with explicit GPIO pins."""
try:
i2c = machine.I2C(0, sda=machine.Pin(SDA_PIN), scl=machine.Pin(SCL_PIN), freq=I2C_FREQ)
return i2c
except Exception as e:
print(f"[FATAL] Failed to initialize I2C bus: {e}")
return None
def scan_and_verify(i2c, target_addr):
"""Scan bus and verify BME280 Chip ID."""
try:
# i2c.scan() gracefully handles NACKs and returns a list of found addresses
devices = i2c.scan()
if not devices:
print("[WARN] No I2C devices found. Check wiring and pull-up resistors.")
return False
print(f"[INFO] Found devices at: {[hex(d) for d in devices]}")
if target_addr in devices:
# Read WHO_AM_I register (0xD0) for BME280
# readfrom_mem will throw OSError if the device drops off mid-transaction
chip_id = i2c.readfrom_mem(target_addr, 0xD0, 1)
if chip_id[0] == 0x60:
print(f"[SUCCESS] BME280 verified. Chip ID: {hex(chip_id[0])}")
return True
else:
print(f"[WARN] Device found but wrong Chip ID: {hex(chip_id[0])} (Expected: 0x60)")
return False
else:
print(f"[WARN] Target address {hex(target_addr)} not found on bus.")
return False
except OSError as e:
# Catching the exact I2C timeout/device not found error
print(f"[ERROR] I2C Communication Failed: {e}")
return False
# Main execution loop
if __name__ == "__main__":
bus = init_i2c()
if bus:
while True:
scan_and_verify(bus, BME280_ADDR)
time.sleep(3)
else:
print("System halted due to I2C initialization failure.")
Debugging: Fixing "OSError: [Errno 19] ENODEV"
When working with I2C in MicroPython on the ESP32-S3, the most common failure mode is the bus timing out or receiving a NACK (Not Acknowledged) from the target device. This manifests in the REPL as:
OSError: [Errno 19] ENODEV
This error means "No such device" — the ESP32 sent data to the bus, but no peripheral pulled the SDA line low to acknowledge it. Here are the ranked causes and the first three things to check when it fails:
- Verify Pull-Up Resistors: I2C is an open-drain protocol. It requires pull-up resistors on SDA and SCL. The Adafruit BME280 (PID 2652) has 10kΩ pull-ups built-in. If you are using a cheap generic clone, it may lack them. Measure the SDA/SCL lines with a multimeter; they should read ~3.28V when idle. If they read 0V or float, add external 4.7kΩ pull-ups to 3V3.
- Confirm the I2C Address: The Adafruit BME280 defaults to
0x77, while many generic Chinese clones default to0x76. Runi2c.scan()in the REPL to see which address your specific board actually responds to, and update theBME280_ADDRvariable. - Check Logic Level Voltage: If you accidentally wired the BME280 VIN to the 5V pin, the sensor's internal logic might be outputting 5V on the SDA line. The ESP32-S3 GPIO pins are not 5V tolerant. Disconnect immediately. You may have already damaged GPIO25.
Secondary Causes:
- Wire Length/Capacitance: I2C degrades rapidly over long wire runs. Keep SDA/SCL jumper wires under 30cm (12 inches). If you must go longer, drop the
I2C_FREQin the code from 400000 to 100000 (100kHz Standard Mode). - USB Power Brownouts: The ESP32-S3 can draw up to 500mA during WiFi transmission spikes. If your PC's USB port cannot supply this, the 3.3V LDO on the Nano ESP32 will brownout, resetting the I2C peripheral mid-transaction. Use a powered USB hub or a dedicated 5V/2A wall adapter.
Extending and Simplifying the Build
Depending on your project goals, you may want to strip this down to the bare minimum or scale it up to an IoT node.
How to Simplify
If you don't have an I2C sensor on hand and just want to verify your MicroPython environment, delete the I2C code and read the ESP32-S3's internal temperature sensor. Add this to your REPL:
import esp32
print(f"Internal MCU Temp: {esp32.raw_temperature()}°F")
print(f"Hall Sensor: {esp32.hall_sensor()}")
This requires zero external wiring and confirms your firmware is executing correctly.
How to Extend
To turn this into a functional IoT weather station, leverage the ESP32-S3's native WiFi. Use the built-in network module to connect to your router, and the umqtt.simple library to publish the BME280 data to a local Mosquitto broker or Home Assistant. You will need to parse the raw I2C compensation registers (detailed in the Bosch BME280 Datasheet) to convert the raw ADC counts into actual Celsius and hPa values, or install a pre-compiled bme280.py driver via Thonny's package manager.
Frequently Asked Questions
Can I run Python on an Arduino Uno R3 or Nano v3?
No. The ATmega328P chip on the Uno R3 and classic Nano v3 has only 2KB of SRAM and runs at 16MHz. A Python interpreter requires at least 256KB of RAM to function. To use Python with an Uno, you must write C++ firmware on the Uno that outputs serial data, and write a Python script on your PC or Raspberry Pi using the pyserial library to read that serial stream. If you want Python on the board, you must upgrade to an ARM/RISC-V based board like the Nano ESP32 or Nano RP2040 Connect.
Is MicroPython on Arduino slower than C++?
Yes, significantly. MicroPython is an interpreted language running on a virtual machine. Expect math-heavy operations (like floating-point sensor compensation) to run 10x to 50x slower than compiled C++ in the Arduino IDE. However, for I/O tasks like toggling GPIOs, reading I2C, or sending MQTT packets over WiFi, the ESP32-S3's 240MHz dual-core processor is so fast that the Python overhead is practically unnoticeable for DIY applications. If you need microsecond-precise bit-banging, stick to C++.
How do I install third-party Python libraries on the Nano ESP32?
MicroPython includes a package manager called mip (MicroPython Install Package). Connect your board to Thonny, open the REPL, and type:
import mip
mip.install("umqtt.simple")
This will download the library directly from the MicroPython GitHub repositories and save it to the board's internal flash filesystem. Alternatively, you can manually download .py files and use Thonny's file explorer to drag and drop them into the root directory of the ESP32.
Why does my Arduino Nano ESP32 show up as a different COM port after flashing?
This is normal behavior for the ESP32-S3. When you hold the B0 button and reset, the chip enters the ROM bootloader mode, which enumerates on the USB bus with a specific Vendor ID/Product ID (VID/PID). Once MicroPython is flashed and the board reboots normally, it uses the TinyUSB CDC driver, which presents a different VID/PID to your operating system. Your OS assigns a new COM port number (e.g., jumping from COM3 to COM4). Always verify your port in the Thonny interpreter settings after a reboot.






