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}")

On this page

Deploy your PySerial test scripts with TofuPilot

Trusted by Lyft, Cobot, Pacific Defense, and more.

TofuPilot provides dashboards and analytics to track serial device testing at scale.

TofuPilot
Get Started