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.

Project Difficulty Rating: Intermediate (3/5)
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
3V3N/AVINPower (3.3V)
GNDN/AGNDCommon Ground
A4GPIO 25SDII2C Data (SDA)
A5GPIO 33SCKI2C 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.

  1. Download the Firmware: Go to the official MicroPython download page and grab the latest stable .bin release for the ARDUINO_NANO_ESP32.
  2. 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., COM3 on Windows or /dev/cu.usbmodem* on macOS).
  3. Erase and Flash: Open your terminal and use esptool (install via pip 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
  4. 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.
  5. 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:

🔧 The First 3 Things to Check:
  1. 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.
  2. Confirm the I2C Address: The Adafruit BME280 defaults to 0x77, while many generic Chinese clones default to 0x76. Run i2c.scan() in the REPL to see which address your specific board actually responds to, and update the BME280_ADDR variable.
  3. 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_FREQ in 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.