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.

Exact Error String:
serial.serialutil.SerialException: could not open port 'COM3': PermissionError(13, 'Access is denied.', None, 5)

Ranked Causes for 'Access Denied':

  1. 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.
  2. A zombie Python process is holding the port. If your previous script crashed without hitting the finally block, the OS still thinks the port is in use. Kill the Python task in Task Manager or reboot the machine.
  3. 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:

  1. 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.
  2. 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 a time.sleep(2) after opening the port.
  3. 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 to 115200.

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.