To run Python with Arduino, you do not execute Python directly on the standard AVR microcontroller. Instead, you run Python on your host PC and communicate with the Arduino over USB UART using the pyserial library. The Arduino handles deterministic, real-time hardware I/O (reading sensors, toggling pins), while Python handles the heavy lifting: data logging, GUI rendering, or machine learning inference. This guide details a robust, production-style serial link between an Arduino Uno R3 and a Python host script, including the exact error handling required to prevent buffer crashes.
Hardware & Software Bill of Materials
This build targets the classic Arduino Uno R3 (ATmega328P) due to its native USB-to-Serial bridge (ATmega16U2), which enumerates reliably across Windows, Linux, and macOS without requiring custom CH340 drivers. We will read environmental data to demonstrate a continuous serial stream.
| Component | Exact Model / Variant | Key Specification | Approx. Cost (2026) |
|---|---|---|---|
| Microcontroller | Arduino Uno R3 (ATmega328P) | 16MHz, 5V logic, native USB CDC | $27.00 |
| Sensor | DHT22 (AM2302) Module | Temp/Humidity, 3.3V-5V, 0.5Hz max | $9.50 |
| Resistor | 10kΩ Pull-up (if using raw DHT22) | 1/4W, 5% tolerance | $0.10 |
| Cable | USB-B to USB-A (Data+Power) | Must have D+/D- data lines | $6.00 |
| Python Env | Python 3.11+ & PySerial 3.5 | Host-side serial parsing | Free |
Pin Mapping & Sensor Wiring
The DHT22 requires a single digital pin for its one-wire protocol. If you are using a bare DHT22 sensor rather than a pre-wired module with an onboard pull-up resistor, you must wire a 10kΩ resistor between VCC and the DATA pin.
| Arduino Uno R3 Pin | DHT22 Module Pin | Wire Color | Notes |
|---|---|---|---|
| 5V | VCC (Pin 1) | Red | Do not use 3.3V; DHT22 needs 5V for stable reads on Uno |
| GND | GND (Pin 4) | Black | Common ground reference |
| D2 | DATA (Pin 2) | Yellow | Requires 10k pull-up if not on module PCB |
| N/A | NC (Pin 3) | N/A | Not connected |
Arduino Firmware: Real-Time Data Acquisition
The Arduino sketch below initializes the DHT22 and outputs data as a Comma-Separated Values (CSV) string. CSV is preferred over JSON for low-bandwidth AVR boards because it avoids the memory overhead of string concatenation and JSON serialization libraries. We set the baud rate to 115200 to ensure the serial buffer clears faster than the sensor polling rate.
#include <DHT.h>
// Pin definitions for Arduino Uno R3
#define DHTPIN 2
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
void setup() {
// Initialize serial at 115200 baud for fast host parsing
Serial.begin(115200);
dht.begin();
}
void loop() {
// DHT22 requires ~2 seconds between reads
delay(2000);
float humidity = dht.readHumidity();
float tempC = dht.readTemperature();
// Check if any reads failed and exit early (to try again)
if (isnan(humidity) || isnan(tempC)) {
Serial.println("error,error");
return;
}
// Output strict CSV format: temp,humidity
Serial.print(tempC);
Serial.print(",");
Serial.println(humidity);
}
Python Host Script: Reading and Parsing Serial Data
This Python script uses pyserial to open the COM port, wait for the Arduino's auto-reset cycle, and continuously parse the CSV stream. It includes critical error handling for UnicodeDecodeError, which frequently occurs when the Python script connects mid-byte and misaligns the serial buffer.
import serial
import time
import sys
# Target: Arduino Uno R3 (ATmega328P)
# Windows: 'COM3' | Linux: '/dev/ttyACM0' | macOS: '/dev/cu.usbmodem...'
PORT = 'COM3'
BAUD_RATE = 115200
def main():
try:
# timeout=1 prevents readline() from blocking forever if Arduino halts
ser = serial.Serial(PORT, BAUD_RATE, timeout=1)
# Wait 2 seconds for the Arduino Uno's auto-reset capacitor to settle
time.sleep(2)
print(f"Connected to {PORT} at {BAUD_RATE} baud.")
except serial.SerialException as e:
print(f"Failed to open port: {e}")
sys.exit(1)
try:
while True:
raw_line = ser.readline()
if raw_line:
try:
# Decode bytes to string and strip newline characters
decoded = raw_line.decode('utf-8').strip()
# Skip error states sent by Arduino
if decoded == "error,error":
print("Sensor read fault. Retrying...")
continue
parts = decoded.split(',')
if len(parts) == 2:
temp = float(parts[0])
hum = float(parts[1])
print(f"Temp: {temp:.1f}C | Humidity: {hum:.1f}%")
except (UnicodeDecodeError, ValueError):
# Catch misaligned serial buffers or corrupt bytes silently
continue
except KeyboardInterrupt:
print("\nInterrupted by user. Closing port...")
finally:
if 'ser' in locals() and ser.is_open:
ser.close()
print("Serial port closed.")
if __name__ == "__main__":
main()
Debugging PySerial Connection Failures
Serial communication is notoriously fragile. When your Python script fails to connect, it almost always throws a specific SerialException. Below is the exact error string and the ranked causes to resolve it.
serial.serialutil.SerialException: could not open port 'COM3': PermissionError(13, 'Access is denied.', None, 5)
Ranked Causes for 'Access Denied':
- The Arduino IDE Serial Monitor is open. The IDE locks the COM port exclusively. Close the Serial Monitor tab in the Arduino IDE before running your Python script.
- A zombie Python process is holding the port. If your previous script crashed without hitting the
finallyblock, the OS still thinks the port is in use. Kill the Python task in Task Manager or reboot the machine. - Another terminal (PuTTY, TeraTerm) is connected. Only one application can claim a CDC-ACM serial port at a time.
The First Three Things to Check When It Fails
If you aren't getting an 'Access Denied' error, but rather no data or a 'Port Not Found' error, run through this checklist:
- Verify the USB Cable has Data Lines: Over 40% of USB-B cables in a maker's drawer are 'charge-only' cables meant for printers or lamps. They lack the D+ and D- wires. If your PC doesn't play the USB connection chime and no COM port appears in Device Manager, swap the cable.
- Check for the Auto-Reset Delay: The Arduino Uno R3 resets every time a serial connection is opened (due to the DTR line toggling). If your Python script sends data immediately after
serial.Serial(), it will be lost during the 1.5-second bootloader window. Always include atime.sleep(2)after opening the port. - Confirm Baud Rate Symmetry: A mismatch (e.g., Arduino at 9600, Python at 115200) won't throw an error; it will just output garbage characters like
ÿÿÿ. Verify both sides are hardcoded to115200.
Scaling the Build: Simplify or Extend
Once you have a stable UART link, you will eventually hit the limits of a direct USB tether. Here is how to adapt the architecture based on your project constraints.
How to Simplify (Skip the C++)
If you do not want to write and flash Arduino C++ firmware, use the Telemetrix or pyFirmata libraries. These allow Python to directly control the Arduino's GPIO pins over serial using the StandardFirmata protocol.
Trade-off: Firmata adds ~30ms of latency per pin read and consumes significant AVR SRAM. It is fine for toggling relays, but unacceptable for high-speed encoder reading or PID control loops.
How to Extend (Cut the Cord)
For multi-room sensor networks or remote logging, USB UART is a bottleneck. Upgrade your hardware to an ESP32-WROOM-32 and switch from PySerial to MQTT over WiFi.
The Architecture Shift: The ESP32 publishes sensor JSON payloads to a local Mosquitto broker. Your Python script subscribes to the MQTT topic using the paho-mqtt library. This decouples the hardware from the host PC, allowing your Python script to run on a cloud VPS or a headless Raspberry Pi while the ESP32 sits in a remote enclosure.
Reference Note: For deeper reading on serial protocol edge cases, consult the official PySerial API documentation and the Arduino Serial Reference. Always remember that local electrical codes and safety standards apply when scaling these microcontroller projects to control mains-voltage relays or contactors.






