PySerial
PySerialDocs

PySerial Configuration Guide

Complete guide to PySerial port settings: baud rate, data bits, parity, stop bits, flow control, and timeouts. Optimize for your hardware.

Master all PySerial port settings and parameters for reliable communication.

Configuration Overview

Basic Settings

Port, baud rate, timeouts

Data Format

Data bits, parity, stop bits

Flow Control

Hardware and software flow control

Advanced Options

Buffers, exclusive access, special modes

Basic Configuration

Constructor Parameters

import serial

ser = serial.Serial(
    port='/dev/ttyUSB0',           # Device path
    baudrate=9600,                 # Communication speed
    bytesize=serial.EIGHTBITS,     # Data bits (5, 6, 7, 8)
    parity=serial.PARITY_NONE,     # Parity checking
    stopbits=serial.STOPBITS_ONE,  # Stop bits (1, 1.5, 2)
    timeout=1,                     # Read timeout in seconds
    xonxoff=False,                 # Software flow control
    rtscts=False,                  # Hardware (RTS/CTS) flow control
    write_timeout=None,            # Write timeout in seconds
    dsrdtr=False,                  # DSR/DTR flow control
    inter_byte_timeout=None,       # Inter-byte timeout
    exclusive=None                 # Exclusive access (Linux)
)

Port Selection

import serial.tools.list_ports

def find_port_by_description(description_filter):
    """Find port by device description"""
    for port in serial.tools.list_ports.comports():
        if description_filter.lower() in port.description.lower():
            return port.device
    return None

def find_port_by_vid_pid(vid, pid):
    """Find port by Vendor ID and Product ID"""
    for port in serial.tools.list_ports.comports():
        if port.vid == vid and port.pid == pid:
            return port.device
    return None

# Usage examples
arduino_port = find_port_by_description("Arduino")
ftdi_port = find_port_by_vid_pid(0x0403, 0x6001)  # FTDI chip

if arduino_port:
    ser = serial.Serial(arduino_port, 9600)
import platform

def get_default_port():
    """Get platform-appropriate default port"""
    system = platform.system()
    
    if system == "Windows":
        return "COM1"
    elif system == "Darwin":  # macOS
        return "/dev/cu.usbserial"
    else:  # Linux and others
        return "/dev/ttyUSB0"

# Platform-specific port patterns
if platform.system() == "Windows":
    ports_to_try = ["COM1", "COM3", "COM4"]
elif platform.system() == "Darwin":
    ports_to_try = ["/dev/cu.usbserial", "/dev/cu.usbmodem*"]
else:
    ports_to_try = ["/dev/ttyUSB0", "/dev/ttyACM0", "/dev/ttyS0"]
class SerialConfig:
    """Serial port configuration manager"""
    
    COMMON_BAUDS = [9600, 19200, 38400, 57600, 115200, 230400, 460800]
    
    def __init__(self):
        self.config = {
            'port': None,
            'baudrate': 9600,
            'bytesize': 8,
            'parity': 'N',
            'stopbits': 1,
            'timeout': 1,
            'write_timeout': None,
            'rtscts': False,
            'dsrdtr': False,
            'xonxoff': False
        }
    
    def set_port(self, port):
        self.config['port'] = port
        return self
    
    def set_baudrate(self, baudrate):
        if baudrate in self.COMMON_BAUDS:
            self.config['baudrate'] = baudrate
        else:
            print(f"⚠️  Uncommon baud rate: {baudrate}")
            self.config['baudrate'] = baudrate
        return self
    
    def set_format(self, data_bits=8, parity='N', stop_bits=1):
        self.config['bytesize'] = data_bits
        self.config['parity'] = parity
        self.config['stopbits'] = stop_bits
        return self
    
    def create_serial(self):
        return serial.Serial(**self.config)

# Usage
config = SerialConfig()
ser = (config
       .set_port('/dev/ttyUSB0')
       .set_baudrate(115200)
       .set_format(8, 'N', 1)
       .create_serial())

Baud Rate Configuration

Common Baud Rates

# Standard baud rates (widely supported)
STANDARD_BAUDS = [
    1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200
]

# High-speed rates (platform/hardware dependent)
HIGH_SPEED_BAUDS = [
    230400, 460800, 921600, 1000000, 1500000, 2000000
]

# Test baud rate compatibility
def test_baud_rate(port, baudrate):
    """Test if baud rate works with device"""
    try:
        test_ser = serial.Serial(port, baudrate, timeout=1)
        
        # Send test command
        test_ser.write(b'AT\r\n')
        response = test_ser.readline()
        
        test_ser.close()
        
        return len(response) > 0
    except:
        return False

# Find optimal baud rate
def find_optimal_baudrate(port, test_bauds=None):
    """Find highest working baud rate"""
    if test_bauds is None:
        test_bauds = STANDARD_BAUDS + HIGH_SPEED_BAUDS
    
    working_bauds = []
    
    for baud in sorted(test_bauds):
        if test_baud_rate(port, baud):
            working_bauds.append(baud)
            print(f"✅ {baud} baud: OK")
        else:
            print(f"❌ {baud} baud: Failed")
    
    return max(working_bauds) if working_bauds else None

Baud Rate Calculation

Baud Rate vs Bit Rate: Baud rate is symbols per second. For simple serial, 1 symbol = 1 bit, so they're equal. But with encoding/modulation, they can differ.

def calculate_transfer_time(data_size, baudrate, data_bits=8, parity_bits=0, stop_bits=1):
    """Calculate time to transfer data"""
    
    # Total bits per byte
    bits_per_byte = 1 + data_bits + parity_bits + stop_bits  # 1 start bit
    
    # Total bits to transfer
    total_bits = data_size * bits_per_byte
    
    # Transfer time in seconds
    transfer_time = total_bits / baudrate
    
    return {
        'bits_per_byte': bits_per_byte,
        'total_bits': total_bits,
        'transfer_time': transfer_time,
        'bytes_per_second': baudrate / bits_per_byte
    }

# Example calculation
stats = calculate_transfer_time(1024, 9600)  # 1KB at 9600 baud
print(f"1KB transfer time at 9600 baud: {stats['transfer_time']:.2f} seconds")
print(f"Effective throughput: {stats['bytes_per_second']:.0f} bytes/second")

Data Format Settings

Data Bits Configuration

# Data bits options
ser.bytesize = serial.FIVEBITS    # 5 bits (rare)
ser.bytesize = serial.SIXBITS     # 6 bits (rare)
ser.bytesize = serial.SEVENBITS   # 7 bits (ASCII)
ser.bytesize = serial.EIGHTBITS   # 8 bits (most common)

# Or use integers
ser.bytesize = 8  # Same as EIGHTBITS

Parity Configuration

# Parity options
ser.parity = serial.PARITY_NONE   # No parity (most common)
ser.parity = serial.PARITY_EVEN   # Even parity
ser.parity = serial.PARITY_ODD    # Odd parity
ser.parity = serial.PARITY_MARK   # Always 1
ser.parity = serial.PARITY_SPACE  # Always 0

# Or use characters
ser.parity = 'N'  # None
ser.parity = 'E'  # Even
ser.parity = 'O'  # Odd
ser.parity = 'M'  # Mark
ser.parity = 'S'  # Space

# Parity checking function
def check_parity(data_byte, parity_type):
    """Check if parity bit is correct"""
    bit_count = bin(data_byte).count('1')
    
    if parity_type == 'E':  # Even
        return bit_count % 2 == 0
    elif parity_type == 'O':  # Odd
        return bit_count % 2 == 1
    elif parity_type == 'M':  # Mark (always 1)
        return True
    elif parity_type == 'S':  # Space (always 0)
        return False
    else:  # None
        return True

Stop Bits Configuration

# Stop bits options
ser.stopbits = serial.STOPBITS_ONE             # 1 stop bit (most common)
ser.stopbits = serial.STOPBITS_ONE_POINT_FIVE  # 1.5 stop bits (rare)
ser.stopbits = serial.STOPBITS_TWO             # 2 stop bits

# Or use numbers
ser.stopbits = 1    # 1 stop bit
ser.stopbits = 1.5  # 1.5 stop bits
ser.stopbits = 2    # 2 stop bits

Format Validation

def validate_format(data_bits, parity, stop_bits):
    """Validate serial format combination"""
    
    valid_data_bits = [5, 6, 7, 8]
    valid_parity = ['N', 'E', 'O', 'M', 'S']
    valid_stop_bits = [1, 1.5, 2]
    
    errors = []
    
    if data_bits not in valid_data_bits:
        errors.append(f"Invalid data bits: {data_bits}")
    
    if parity not in valid_parity:
        errors.append(f"Invalid parity: {parity}")
    
    if stop_bits not in valid_stop_bits:
        errors.append(f"Invalid stop bits: {stop_bits}")
    
    # Special combinations
    if data_bits == 5 and stop_bits == 1.5:
        errors.append("5 data bits with 1.5 stop bits not recommended")
    
    return len(errors) == 0, errors

# Example usage
is_valid, errors = validate_format(8, 'N', 1)
if is_valid:
    print("✅ Valid format: 8N1")
else:
    for error in errors:
        print(f"❌ {error}")

Timeout Configuration

Read Timeouts

# Timeout modes
ser.timeout = None    # Blocking - wait forever
ser.timeout = 0       # Non-blocking - return immediately
ser.timeout = 1.5     # Wait maximum 1.5 seconds

# Different timeout for different operations
def read_with_custom_timeout(ser, size, timeout):
    """Temporarily change timeout"""
    old_timeout = ser.timeout
    ser.timeout = timeout
    
    try:
        data = ser.read(size)
        return data
    finally:
        ser.timeout = old_timeout

# Usage
quick_data = read_with_custom_timeout(ser, 10, 0.1)  # 100ms timeout
slow_data = read_with_custom_timeout(ser, 100, 10)   # 10s timeout
# Inter-byte timeout
ser.timeout = 5              # Overall timeout
ser.inter_byte_timeout = 0.1  # Max 100ms between bytes

# This is useful for variable-length messages where
# bytes arrive continuously but there are gaps between messages

# Write timeout
ser.write_timeout = 2  # Timeout for write operations

# Example: Streaming data reader
def stream_reader(ser):
    """Read stream with inter-byte timeout"""
    ser.timeout = 10           # Long overall timeout
    ser.inter_byte_timeout = 0.05  # 50ms max gap
    
    while True:
        data = ser.read(1000)  # Try to read up to 1000 bytes
        
        if not data:
            print("Stream ended (timeout)")
            break
        
        print(f"Got {len(data)} bytes")
        process_data(data)
class AdaptiveTimeout:
    """Automatically adjust timeout based on performance"""
    
    def __init__(self, ser, initial_timeout=1.0):
        self.ser = ser
        self.timeout = initial_timeout
        self.success_count = 0
        self.timeout_count = 0
        
    def read_adaptive(self, size):
        """Read with adaptive timeout adjustment"""
        self.ser.timeout = self.timeout
        start_time = time.time()
        
        data = self.ser.read(size)
        read_time = time.time() - start_time
        
        if len(data) == size:
            # Successful read
            self.success_count += 1
            self.timeout_count = 0
            
            # If consistently successful, reduce timeout
            if self.success_count > 10:
                self.timeout = max(0.1, self.timeout * 0.9)
                self.success_count = 0
                print(f"🔄 Reduced timeout to {self.timeout:.2f}s")
        else:
            # Timeout occurred
            self.timeout_count += 1
            self.success_count = 0
            
            # Increase timeout after repeated failures
            if self.timeout_count > 3:
                self.timeout = min(10.0, self.timeout * 1.5)
                self.timeout_count = 0
                print(f"🔄 Increased timeout to {self.timeout:.2f}s")
        
        return data

# Usage
adaptive = AdaptiveTimeout(ser)

for i in range(100):
    data = adaptive.read_adaptive(50)
    if data:
        print(f"Read {len(data)} bytes")

Flow Control

Software Flow Control (XON/XOFF)

# Enable software flow control
ser = serial.Serial(
    port='/dev/ttyUSB0',
    baudrate=9600,
    xonxoff=True  # Enable XON/XOFF flow control
)

# XON/XOFF control characters
XON = b'\x11'   # Resume transmission (Ctrl+Q)
XOFF = b'\x13'  # Pause transmission (Ctrl+S)

# Manual XON/XOFF handling (if xonxoff=False)
def manual_flow_control_example(ser):
    """Example of manual flow control handling"""
    
    while True:
        # Check for incoming data
        if ser.in_waiting:
            data = ser.read(1)
            
            if data == XOFF:
                print("⏸️  Received XOFF - pausing transmission")
                # Pause sending data
                continue
            elif data == XON:
                print("▶️  Received XON - resuming transmission")
                # Resume sending data
                continue
            else:
                # Process normal data
                process_data(data)

Hardware Flow Control (RTS/CTS)

# Enable hardware flow control
ser = serial.Serial(
    port='/dev/ttyUSB0',
    baudrate=9600,
    rtscts=True  # Enable RTS/CTS flow control
)

# Manual RTS/CTS control
class RTSCTSControl:
    def __init__(self, ser):
        self.ser = ser
    
    def wait_for_clear_to_send(self, timeout=5):
        """Wait for CTS signal"""
        start_time = time.time()
        
        while not self.ser.cts:
            if time.time() - start_time > timeout:
                raise TimeoutError("CTS timeout")
            time.sleep(0.01)
        
        return True
    
    def controlled_write(self, data):
        """Write data with CTS checking"""
        # Request to send
        self.ser.rts = True
        
        try:
            # Wait for clear to send
            self.wait_for_clear_to_send()
            
            # Send data
            bytes_written = self.ser.write(data)
            self.ser.flush()
            
            return bytes_written
            
        finally:
            # Clear request to send
            self.ser.rts = False

# Usage
rts_cts = RTSCTSControl(ser)
rts_cts.controlled_write(b"Data with flow control")

DSR/DTR Flow Control

# Enable DSR/DTR flow control
ser = serial.Serial(
    port='/dev/ttyUSB0',
    baudrate=9600,
    dsrdtr=True  # Enable DSR/DTR flow control
)

# Manual DTR control
def modem_control_example(ser):
    """Example of DTR/DSR modem control"""
    
    # Assert DTR (Data Terminal Ready)
    ser.dtr = True
    print("✅ DTR asserted")
    
    # Wait for DSR (Data Set Ready)
    timeout = 10
    start_time = time.time()
    
    while not ser.dsr:
        if time.time() - start_time > timeout:
            print("❌ DSR timeout")
            return False
        time.sleep(0.1)
    
    print("✅ DSR detected - modem ready")
    
    # Check carrier detect
    if ser.cd:
        print("✅ Carrier detected")
    
    # Check ring indicator
    if ser.ri:
        print("📞 Ring indicator active")
    
    return True

# Usage
if modem_control_example(ser):
    print("Modem handshake successful")

Buffer Configuration

Buffer Sizes

# Set buffer sizes (platform dependent)
try:
    ser.set_buffer_size(rx_size=8192, tx_size=4096)
    print("✅ Buffer sizes set")
except AttributeError:
    print("ℹ️  Buffer size control not available on this platform")

# Monitor buffer usage
def monitor_buffers(ser, duration=10):
    """Monitor buffer usage over time"""
    start_time = time.time()
    max_input_waiting = 0
    
    while time.time() - start_time < duration:
        waiting = ser.in_waiting
        max_input_waiting = max(max_input_waiting, waiting)
        
        if waiting > 0:
            print(f"📊 Input buffer: {waiting} bytes")
        
        time.sleep(1)
    
    print(f"📈 Maximum buffer usage: {max_input_waiting} bytes")

Buffer Management

class BufferManager:
    """Advanced buffer management"""
    
    def __init__(self, ser, warning_threshold=0.8, critical_threshold=0.95):
        self.ser = ser
        self.warning_threshold = warning_threshold
        self.critical_threshold = critical_threshold
        self.buffer_size = 4096  # Assume default size
        
    def check_buffer_health(self):
        """Check buffer status and warn if needed"""
        waiting = self.ser.in_waiting
        usage_ratio = waiting / self.buffer_size
        
        if usage_ratio > self.critical_threshold:
            print(f"🚨 CRITICAL: Input buffer {usage_ratio:.1%} full ({waiting} bytes)")
            return 'critical'
        elif usage_ratio > self.warning_threshold:
            print(f"⚠️  WARNING: Input buffer {usage_ratio:.1%} full ({waiting} bytes)")
            return 'warning'
        else:
            return 'ok'
    
    def emergency_clear(self):
        """Clear buffers in emergency"""
        print("🗑️  Emergency buffer clear")
        self.ser.reset_input_buffer()
        self.ser.reset_output_buffer()
    
    def smart_read(self, target_size):
        """Read with buffer management"""
        status = self.check_buffer_health()
        
        if status == 'critical':
            self.emergency_clear()
            return b''
        
        # Read appropriate amount based on buffer status
        available = self.ser.in_waiting
        to_read = min(target_size, available)
        
        return self.ser.read(to_read)

# Usage
buffer_mgr = BufferManager(ser)
data = buffer_mgr.smart_read(1024)

Advanced Configuration

Exclusive Access

# Exclusive access (Linux only)
try:
    ser = serial.Serial(
        port='/dev/ttyUSB0',
        baudrate=9600,
        exclusive=True  # Prevent other processes from using port
    )
    print("✅ Exclusive access granted")
except ValueError:
    print("⚠️  Exclusive access not supported on this platform")
except serial.SerialException as e:
    print(f"❌ Exclusive access failed: {e}")

Custom Configuration Profiles

class SerialProfiles:
    """Predefined configuration profiles"""
    
    # Common device profiles
    PROFILES = {
        'arduino': {
            'baudrate': 9600,
            'bytesize': 8,
            'parity': 'N',
            'stopbits': 1,
            'timeout': 2,
            'write_timeout': 2
        },
        'gps': {
            'baudrate': 4800,
            'bytesize': 8,
            'parity': 'N',
            'stopbits': 1,
            'timeout': 1,
            'inter_byte_timeout': 0.1
        },
        'modem': {
            'baudrate': 115200,
            'bytesize': 8,
            'parity': 'N',
            'stopbits': 1,
            'timeout': 5,
            'rtscts': True
        },
        'industrial': {
            'baudrate': 9600,
            'bytesize': 8,
            'parity': 'E',  # Even parity for error detection
            'stopbits': 1,
            'timeout': 1,
            'write_timeout': 1
        }
    }
    
    @classmethod
    def create_serial(cls, port, profile_name):
        """Create serial connection with profile"""
        if profile_name not in cls.PROFILES:
            raise ValueError(f"Unknown profile: {profile_name}")
        
        config = cls.PROFILES[profile_name].copy()
        config['port'] = port
        
        return serial.Serial(**config)
    
    @classmethod
    def list_profiles(cls):
        """List available profiles"""
        for name, config in cls.PROFILES.items():
            print(f"{name}: {config['baudrate']} baud, "
                  f"{config['bytesize']}{config['parity']}{config['stopbits']}")

# Usage
SerialProfiles.list_profiles()
arduino = SerialProfiles.create_serial('/dev/ttyACM0', 'arduino')
gps = SerialProfiles.create_serial('/dev/ttyUSB0', 'gps')

Configuration Validation

def validate_serial_config(port, **config):
    """Validate serial configuration before connecting"""
    
    errors = []
    warnings = []
    
    # Check port existence
    if not port:
        errors.append("Port not specified")
    else:
        # Check if port exists
        available_ports = [p.device for p in serial.tools.list_ports.comports()]
        if port not in available_ports:
            warnings.append(f"Port {port} not found in available ports")
    
    # Validate baud rate
    baudrate = config.get('baudrate', 9600)
    if baudrate <= 0:
        errors.append("Baud rate must be positive")
    elif baudrate > 2000000:
        warnings.append(f"Very high baud rate: {baudrate}")
    
    # Validate timeouts
    timeout = config.get('timeout')
    if timeout is not None and timeout < 0:
        errors.append("Timeout cannot be negative")
    
    # Check for conflicting flow control
    xonxoff = config.get('xonxoff', False)
    rtscts = config.get('rtscts', False)
    dsrdtr = config.get('dsrdtr', False)
    
    flow_control_count = sum([xonxoff, rtscts, dsrdtr])
    if flow_control_count > 1:
        warnings.append("Multiple flow control methods enabled")
    
    return {
        'valid': len(errors) == 0,
        'errors': errors,
        'warnings': warnings
    }

# Usage
validation = validate_serial_config(
    port='/dev/ttyUSB0',
    baudrate=115200,
    timeout=1,
    rtscts=True
)

if validation['valid']:
    print("✅ Configuration valid")
    for warning in validation['warnings']:
        print(f"⚠️  {warning}")
else:
    print("❌ Configuration invalid:")
    for error in validation['errors']:
        print(f"  - {error}")

Configuration Testing

def test_serial_configuration(port, **config):
    """Test serial configuration with actual hardware"""
    
    print(f"🧪 Testing configuration on {port}")
    print(f"   Settings: {config}")
    
    try:
        # Test connection
        ser = serial.Serial(port, **config)
        print("✅ Port opened successfully")
        
        # Test basic communication
        ser.write(b'AT\r\n')
        ser.flush()
        
        response = ser.read(100)
        
        if response:
            print(f"✅ Communication test: received {len(response)} bytes")
            print(f"   Response: {response[:50]}...")  # First 50 bytes
        else:
            print("⚠️  No response received (may be normal)")
        
        # Test buffer status
        waiting = ser.in_waiting
        print(f"ℹ️  Buffer status: {waiting} bytes waiting")
        
        ser.close()
        print("✅ Port closed successfully")
        
        return True
        
    except Exception as e:
        print(f"❌ Configuration test failed: {e}")
        return False

# Usage
success = test_serial_configuration(
    '/dev/ttyUSB0',
    baudrate=9600,
    timeout=2,
    parity='N'
)

Next Steps

Configuration Mastery: You now understand all PySerial settings and can optimize port configuration for any hardware and application requirements.

Proper configuration is the foundation of reliable serial communication. Match your settings to your hardware specifications and always validate your configuration before production use.

How is this guide?