If you are searching for how to run Python on Arduino, you need to hear a hard truth right up front: standard 8-bit Arduino boards (like the Uno R3 or Nano with the ATmega328P) cannot run Python natively. They have 2KB of RAM and 32KB of Flash, which is barely enough for C++ compiled machine code, let alone a Python interpreter. To run Python directly on the board, you need a 32-bit microcontroller with at least 512KB of RAM.

However, the official Arduino lineup has evolved. By using the Arduino Nano ESP32, you get the exact physical form factor and pinout of a classic Nano, but with an ESP32-S3 chip under the hood that natively supports MicroPython. This guide walks you through setting up Python on this specific board, wiring an I2C sensor, writing robust code, and debugging the inevitable I2C errors.

The "Python on Arduino" Decision Path

Before buying hardware, use this decision matrix to determine the right architecture for your project. We are terminating this decision path with the Arduino Nano ESP32 for native, standalone execution.

If your goal is... Architecture Path Concrete Hardware Pick
Run Python natively on official Arduino hardware (standalone) MicroPython on ESP32-S3 Arduino Nano ESP32 (ABX00092)
Run Python on a PC/Raspberry Pi to control a classic Uno pyFirmata (Python on host, C++ on board) Arduino Uno R3 + Raspberry Pi 4
Run native Python on the cheapest possible board MicroPython on RP2040 Raspberry Pi Pico W (Not Arduino brand)
Run full desktop CPython with heavy math/ML libraries Linux Single Board Computer Raspberry Pi 5 or BeagleBone AI
Decision Verdict: For a true "Python on Arduino" experience where the board operates independently without a PC tether, buy the Arduino Nano ESP32. It costs around $22, fits standard Nano shields, and runs MicroPython flawlessly.

Hardware Spec Sheet & Pin Mapping

The Arduino Nano ESP32 has a critical quirk that bricks components for beginners: it is strictly a 3.3V logic board. Unlike the classic 5V Nano, feeding 5V into its I2C or GPIO pins will destroy the ESP32-S3 silicon. Ensure your sensors are 3.3V tolerant or use a logic level shifter.

Parts List

  • Microcontroller: Arduino Nano ESP32 (Part #ABX00092) - ~$22.00
  • Sensor: Adafruit BME280 I2C/SPI Breakout (Part #2652) - ~$19.95 (Native 3.3V logic)
  • IDE: Thonny IDE (v4.1 or newer) - Free
  • Wiring: 4x M-F jumper wires, half-size breadboard

Pin Mapping Table

The Nano ESP32 silkscreen shows analog pins (A4, A5), but MicroPython requires the native ESP32-S3 GPIO numbers. A4 maps to GPIO5, and A5 maps to GPIO6.

Nano ESP32 Silkscreen Native ESP32-S3 GPIO BME280 Breakout Pin Function
3V3 N/A (Power) VIN 3.3V Power
GND N/A (Ground) GND Common Ground
A4 GPIO 5 SDI I2C SDA (Data)
A5 GPIO 6 SCK I2C SCL (Clock)

Step-by-Step Build: I2C Environmental Logger

Difficulty Rating: 2/5 | Time: 20 Minutes

  1. Flash the Firmware: Open Thonny IDE. Go to Tools > Options > Interpreter. Select MicroPython (ESP32). Click Install or update MicroPython. Select the DFU mode option if prompted, and follow the on-screen instructions to put the Nano ESP32 into bootloader mode (double-tap the reset button).
  2. Wire the I2C Bus: Connect the BME280 to the Nano ESP32 exactly as specified in the pin mapping table above. Double-check that you are using the 3V3 pin, not the VBUS (5V) pin.
  3. Verify I2C Address: The Adafruit BME280 defaults to I2C address 0x77. If you are using a generic clone board, it might be 0x76. We will handle this in the code.
  4. Save the Code: In Thonny, save the Python script below to the MicroPython device as main.py so it runs automatically on boot, or bme_logger.py to run it manually.

The Code: MicroPython BME280 Reader

This script targets the Arduino Nano ESP32. Instead of relying on third-party BME280 libraries that often break between MicroPython versions, this code performs a direct I2C bus scan and reads the BME280's hardware Chip ID register (0xD0). If the wiring is correct, the chip will return 0x60. This is the ultimate proof-of-life test for your I2C bus.


import machine
import time

# --- PIN DEFINITIONS (Arduino Nano ESP32) ---
# Silkscreen A4 is native GPIO5, A5 is native GPIO6
SDA_PIN = 5
SCL_PIN = 6
I2C_FREQ = 100000  # 100kHz standard mode

# BME280 I2C Addresses (Adafruit = 0x77, Generic clones = 0x76)
BME_ADDRESSES = [0x77, 0x76]
BME_CHIP_ID_REG = 0xD0
EXPECTED_CHIP_ID = 0x60

def init_i2c():
    """Initialize I2C bus on Nano ESP32 native GPIOs."""
    i2c = machine.I2C(0, scl=machine.Pin(SCL_PIN), sda=machine.Pin(SDA_PIN), freq=I2C_FREQ)
    return i2c

def find_sensor(i2c):
    """Scan bus and return the first valid BME280 address."""
    devices = i2c.scan()
    print(f"I2C Scan found {len(devices)} device(s): {[hex(d) for d in devices]}")
    
    for addr in BME_ADDRESSES:
        if addr in devices:
            return addr
    return None

def verify_chip_id(i2c, addr):
    """Read the Chip ID register to confirm it is actually a BME280."""
    try:
        # Write the register pointer, then read 1 byte back
        i2c.writeto(addr, bytes([BME_CHIP_ID_REG]))
        chip_id = i2c.readfrom(addr, 1)[0]
        return chip_id
    except OSError as e:
        print(f"I2C Read Error: {e}")
        return None

def main():
    print("Starting Python on Arduino Nano ESP32...")
    i2c = init_i2c()
    
    sensor_addr = find_sensor(i2c)
    if not sensor_addr:
        print("FATAL: BME280 not found on I2C bus. Check wiring.")
        return
    
    print(f"Sensor found at {hex(sensor_addr)}. Verifying Chip ID...")
    chip_id = verify_chip_id(i2c, sensor_addr)
    
    if chip_id == EXPECTED_CHIP_ID:
        print(f"SUCCESS: Valid BME280 detected (Chip ID: {hex(chip_id)}).")
        print("I2C bus is healthy. You can now load the full bme280.py driver.")
    else:
        print(f"WARNING: Device found, but Chip ID is {hex(chip_id)}. Expected {hex(EXPECTED_CHIP_ID)}.")
        print("You may have a different sensor (like a BME680 or BMP280) at this address.")

if __name__ == "__main__":
    main()

Debugging: Fixing "OSError: [Errno 19] ENODEV"

When working with I2C on the ESP32-S3, the most common failure mode is the bus failing to acknowledge the device. If your code crashes, you will likely see this exact error string in the Thonny shell:

OSError: [Errno 19] ENODEV
(Sometimes accompanied by OSError: [Errno 110] ETIMEDOUT depending on the specific MicroPython build version).

This means the ESP32 sent a clock pulse and data byte, but the sensor did not pull the SDA line low to acknowledge (ACK). Here are the first three things to check when this happens:

  1. Check the Power Rail Voltage: Use a multimeter to measure between the BME280 VIN and GND pins. If you read 0V, your breadboard power rail is disconnected. If you read 5V, you are using the VBUS pin instead of 3V3, and you may have already damaged the Nano ESP32's GPIO pins.
  2. Verify the GPIO Mapping: Did you use SDA_PIN = 4 because the silkscreen says A4? The Nano ESP32 requires the native GPIO number (5). If you initialize the I2C object on the wrong GPIO, the ESP32 will look for ACKs on a pin that isn't physically connected to anything, throwing ENODEV.
  3. Inspect the Pull-up Resistors: I2C requires pull-up resistors on SDA and SCL. The Adafruit BME280 breakout has 10kΩ pull-ups built-in. If you are using a bare sensor module without pull-ups, the signals will float, causing timeouts.

Ranked Causes for ENODEV

Rank Root Cause Fix / Measurement
1 Wrong GPIO number in code (Silkscreen vs Native) Change code to GPIO 5 (SDA) and GPIO 6 (SCL).
2 Missing common ground between board and sensor Measure resistance across GND pins; should read < 1 ohm.
3 Sensor is 5V only, ESP32 is outputting 3.3V logic Check sensor datasheet. Add a bi-directional logic level shifter.
4 I2C address mismatch (0x76 vs 0x77) Check the i2c.scan() output in the shell and update the array.

Extending and Simplifying the Build

Once your I2C bus passes the Chip ID verification, you have a stable foundation. Here is how to adapt the project based on your end goal.

How to Extend (Add Connectivity)

The primary advantage of the Nano ESP32 over a classic Nano is the built-in Wi-Fi and Bluetooth. To extend this build into an IoT node:

  • Add MQTT: Use the umqtt.simple library built into MicroPython. Publish the sensor data to an MQTT broker like Mosquitto running on a Raspberry Pi.
  • Implement Deep Sleep: The ESP32-S3 supports deep sleep. Add import machine; machine.deepsleep(60000) at the end of your loop to drop power consumption from ~80mA down to ~10µA, allowing months of runtime on a 2000mAh LiPo battery.

How to Simplify (Reduce Cost and Code)

If the $20 Adafruit BME280 is overkill and you only need basic temperature and humidity (no barometric pressure), swap the sensor for an AHT20 or SHTC3 breakout (typically $4 to $6 on Amazon or AliExpress). The I2C wiring remains identical. You will need to update the I2C address in the code (AHT20 is usually 0x38) and use its specific initialization command sequence, but the physical build and the Nano ESP32 GPIO mapping remain exactly the same.

For official documentation on the board's pinout quirks, always refer to the Arduino Nano ESP32 Cheat Sheet, and for MicroPython I2C specifics, consult the MicroPython machine.I2C documentation. If you need a reliable IDE for flashing and managing files on the ESP32, download Thonny IDE.