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 |
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
- 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).
- 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.
- Verify I2C Address: The Adafruit BME280 defaults to I2C address
0x77. If you are using a generic clone board, it might be0x76. We will handle this in the code. - Save the Code: In Thonny, save the Python script below to the MicroPython device as
main.pyso it runs automatically on boot, orbme_logger.pyto 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 byOSError: [Errno 110] ETIMEDOUTdepending 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:
- 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.
- Verify the GPIO Mapping: Did you use
SDA_PIN = 4because 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. - 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.simplelibrary 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.






