PySerial
PySerialDocs

Linux

Set up PySerial on Linux with device discovery, udev rules for persistent naming, permissions, and serial port troubleshooting.

Linux serial devices appear as /dev/ttyUSB* (USB adapters), /dev/ttyACM* (CDC ACM devices like Arduino), or /dev/ttyS* (built-in UARTs). You need group membership or udev rules to access them without root.

Permissions

The most common PySerial error on Linux is PermissionError: [Errno 13]. Serial devices belong to the dialout group (Debian/Ubuntu) or uucp (Arch).

# Add your user to dialout (log out and back in after this)
sudo usermod -a -G dialout $USER

# Verify membership
groups

If you need access right now without logging out:

# Temporary, for the current shell only
sudo chmod 666 /dev/ttyUSB0

Device Discovery

import serial.tools.list_ports

ports = serial.tools.list_ports.comports()
for port in ports:
    print(f"{port.device}: {port.description}")
    if port.vid:
        print(f"  VID:PID = {port.vid:04x}:{port.pid:04x}")
    if port.serial_number:
        print(f"  Serial: {port.serial_number}")
    if port.manufacturer:
        print(f"  Manufacturer: {port.manufacturer}")
# List serial devices detected by the kernel
ls -la /dev/ttyUSB* /dev/ttyACM* /dev/ttyS* 2>/dev/null

# Detailed USB device info
lsusb

# Get udev attributes for a specific device (useful for writing rules)
udevadm info -a --name=/dev/ttyUSB0

# Watch for plug/unplug events in real time
udevadm monitor --udev --subsystem-match=tty

Log serial data automatically

TofuPilot records test results from your PySerial scripts, tracks pass/fail rates, and generates compliance reports. Free to start.

Find a Device by VID/PID or Serial Number

USB port paths (/dev/ttyUSB0, /dev/ttyUSB1) can change across reboots. To connect reliably, match on hardware identifiers instead.

import serial
import serial.tools.list_ports

def find_by_vid_pid(vid, pid):
    """Find port matching a USB vendor/product ID."""
    for port in serial.tools.list_ports.comports():
        if port.vid == vid and port.pid == pid:
            return port.device
    return None

def find_by_serial(serial_number):
    """Find port matching a USB serial number."""
    for port in serial.tools.list_ports.comports():
        if port.serial_number == serial_number:
            return port.device
    return None

# FTDI FT232
device = find_by_vid_pid(0x0403, 0x6001)
if device:
    ser = serial.Serial(device, 115200, timeout=1)
    print(f"Connected to {device}")
    ser.close()

Udev Rules

Udev rules give your device a persistent symlink name, set permissions automatically, and survive reboots.

Writing a Rule

Create a file in /etc/udev/rules.d/. Higher numbers run later (99 is common for user rules).

# Match by serial number (most reliable)
SUBSYSTEM=="tty", ATTRS{serial}=="A1B2C3D4", SYMLINK+="mydevice", MODE="0666", GROUP="dialout"

# Match by VID/PID
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="ftdi-device", MODE="0666"

# Match by manufacturer and product string
SUBSYSTEM=="tty", ATTRS{manufacturer}=="Arduino", ATTRS{product}=="Arduino Uno", SYMLINK+="arduino"

Applying Rules

# Reload rules
sudo udevadm control --reload-rules
sudo udevadm trigger

# Verify the symlink was created
ls -la /dev/mydevice

Finding the Right Attributes

To see what you can match on for a connected device:

udevadm info -a --name=/dev/ttyUSB0 | grep -E '{serial}|{idVendor}|{idProduct}|{manufacturer}|{product}'
import serial

# Connect via the persistent symlink
ser = serial.Serial('/dev/mydevice', 115200, timeout=1)
ser.write(b'AT\r\n')
print(ser.readline().decode().strip())
ser.close()

Built-in UARTs

Built-in serial ports (/dev/ttyS*, /dev/ttyAMA* on Raspberry Pi) don't need drivers but may need enabling.

Check Available UARTs

# List built-in serial ports
dmesg | grep tty

# Check which ones are real hardware (not just reserved entries)
cat /proc/tty/driver/serial

# Configure with stty
stty -F /dev/ttyS0 115200 cs8 -parenb -cstopb

Loopback Test

Connect TX to RX on the same port to verify the hardware works.

import serial

ser = serial.Serial('/dev/ttyS0', 9600, timeout=1)
ser.reset_input_buffer()

test_data = b"Loopback test"
ser.write(test_data)
ser.flush()

received = ser.read(len(test_data))
if received == test_data:
    print("Loopback OK")
else:
    print(f"Mismatch: sent {test_data!r}, got {received!r}")

ser.close()

Low Latency Mode

For time-sensitive applications, enable low-latency mode on the USB-serial adapter:

# One-time (resets on unplug)
setserial /dev/ttyUSB0 low_latency

# Or via udev rule (persistent)
# Add to your rules file:
# SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", RUN+="/bin/setserial /dev/%k low_latency"

You can also set it from Python using termios:

import serial
import termios

ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=0.01)

# Set VMIN=1, VTIME=0 for minimum latency reads
fd = ser.fileno()
attrs = termios.tcgetattr(fd)
attrs[6][termios.VMIN] = 1
attrs[6][termios.VTIME] = 0
termios.tcsetattr(fd, termios.TCSANOW, attrs)

print("Low-latency mode set")
ser.close()

Troubleshooting

Common Issues

SymptomCauseFix
PermissionError: [Errno 13]Not in dialout groupsudo usermod -a -G dialout $USER, then log out/in
FileNotFoundErrorDevice not plugged in or wrong pathCheck ls /dev/ttyUSB* and dmesg | tail
SerialException: could not open portAnother process has it openfuser /dev/ttyUSB0 to find which process
Port number changed after rebootUSB enumeration order changedUse udev rules for a persistent symlink
No /dev/ttyUSB* after plugging inMissing kernel modulesudo modprobe ftdi_sio (or ch341, cp210x)

Diagnostic Script

import serial
import serial.tools.list_ports
import os
import platform

print(f"Linux {platform.release()}")
print(f"Python {platform.python_version()}")
print(f"PySerial {serial.__version__}")

# Check group membership
groups = os.popen("groups").read().strip()
has_dialout = "dialout" in groups
print(f"User groups: {groups}")
print(f"In dialout group: {has_dialout}")

ports = list(serial.tools.list_ports.comports())
print(f"\n{len(ports)} serial port(s) found:")

for port in ports:
    print(f"\n  {port.device}: {port.description}")
    perms = oct(os.stat(port.device).st_mode)[-3:]
    owner = os.popen(f"stat -c '%U:%G' {port.device}").read().strip()
    print(f"    Permissions: {perms} ({owner})")

    try:
        s = serial.Serial(port.device, 9600, timeout=0.1)
        print(f"    Status: opens OK")
        s.close()
    except PermissionError:
        print(f"    Status: permission denied")
        if not has_dialout:
            print(f"    Fix: sudo usermod -a -G dialout $USER")
    except serial.SerialException as e:
        print(f"    Status: error - {e}")