Modern Python serial port library — a drop-in replacement for pyserial.
- Python 3.10+ with full type annotations (PEP 561)
- Zero dependencies — stdlib only
- Cross-platform — Linux, macOS, Windows
- Built-in async —
AsyncSerialwith native asyncio support - Port discovery —
list_ports()enumerates available serial devices - CLI tools —
wireio-minitermandwireio-list-ports
pyserial is used by 100,000+ projects but has had no release since November 2020. The single maintainer is unreachable, 275+ issues are open, and the library has no type hints, no async support, and still targets Python 2.7.
wireio is a modern replacement:
| pyserial | wireio | |
|---|---|---|
| Last release | Nov 2020 | Active |
| Dependencies | None | None |
| Python support | 2.7, 3.4–3.8 | 3.10+ (including 3.13+) |
| Type hints | No | Full PEP 561 |
| Async support | Separate package | Built-in AsyncSerial |
| API style | Old-style class | Dataclass config + enums |
pip install wireiofrom wireio import Serial
with Serial("/dev/ttyUSB0", baudrate=115200) as port:
port.write(b"AT\r\n")
response = port.read_until(b"\r\n")
print(response)import asyncio
from wireio import AsyncSerial
async def main():
async with AsyncSerial("/dev/ttyUSB0", baudrate=9600) as port:
await port.write(b"hello")
data = await port.read(100)
print(data)
asyncio.run(main())from wireio import list_ports
for port in list_ports():
print(f"{port.device} — {port.description}")from wireio import Serial, SerialConfig, Parity, StopBits
config = SerialConfig(
baudrate=115200,
parity=Parity.EVEN,
stopbits=StopBits.TWO,
timeout=1.0,
)
with Serial("/dev/ttyS0", config=config) as port:
port.write(b"data")Main serial port class. Platform-specific backend is selected automatically (POSIX on Linux/macOS, Win32 on Windows).
Constructor parameters:
port— device path (/dev/ttyUSB0,COM3)baudrate— baud rate (default:9600)bytesize—ByteSize.FIVEthroughByteSize.EIGHT(default:EIGHT)parity—Parity.NONE,EVEN,ODD,MARK,SPACE(default:NONE)stopbits—StopBits.ONE,ONE_POINT_FIVE,TWO(default:ONE)timeout— read timeout in seconds (None= blocking,0= non-blocking, default:None)write_timeout— write timeout in seconds (None= blocking, default:None)flow_control—FlowControl.NONE,HARDWARE,SOFTWARE(default:NONE)xonxoff— enable XON/XOFF software flow control (default:False)rtscts— enable RTS/CTS hardware flow control (default:False)dsrdtr— enable DSR/DTR hardware flow control (default:False)inter_byte_timeout— timeout between consecutive bytes in seconds (None= disabled, default:None)config—SerialConfigobject (overrides individual params when provided)
Methods:
open()/close()— open or close the portread(size=1) -> bytes— read up tosizebytes; returns fewer if timeout expireswrite(data) -> int— write bytes, returns count writtenflush()— wait until all data transmittedread_until(delimiter=b"\n", size=0) -> bytes— read until delimiter found;size=0means no limitread_line() -> bytes— read until\nread_exactly(size) -> bytes— read exactlysizebytes, blocking until all receivedconfigure(config)— apply newSerialConfigto an open port
Properties:
is_open— whether the port is openin_waiting— bytes available in input bufferport,baudrate,bytesize,parity,stopbits,timeout,config
Supports the context manager protocol (with Serial(...) as port:).
Async wrapper around Serial. Runs blocking I/O in a thread executor via asyncio.run_in_executor. Same constructor parameters as Serial, plus:
loop(asyncio.AbstractEventLoop | None) — event loop to use (default: current running loop)
All I/O methods are async and mirror the Serial interface:
async with AsyncSerial("/dev/ttyUSB0", baudrate=9600) as port:
await port.write(b"hello")
data = await port.read(100)
line = await port.read_line()
exact = await port.read_exactly(4)
until = await port.read_until(b"\r\n")
await port.flush()
await port.configure(SerialConfig(baudrate=115200))Async methods: open, close, read, write, flush, read_until, read_line, read_exactly, configure
Properties (sync): port, is_open, baudrate, config
Supports the async context manager protocol (async with AsyncSerial(...) as port:).
Dataclass holding all serial port settings. Pass as the config= argument to Serial or AsyncSerial, or use port.configure(config) to reconfigure an open port.
from wireio import SerialConfig, Parity, StopBits, ByteSize, FlowControl
config = SerialConfig(
baudrate=115200,
bytesize=ByteSize.EIGHT,
parity=Parity.NONE,
stopbits=StopBits.ONE,
timeout=1.0,
write_timeout=None,
flow_control=FlowControl.NONE,
inter_byte_timeout=None,
)Fields (all optional, defaults match Serial constructor):
baudrate: int— default9600bytesize: ByteSize— defaultByteSize.EIGHTparity: Parity— defaultParity.NONEstopbits: StopBits— defaultStopBits.ONEtimeout: float | None— defaultNonewrite_timeout: float | None— defaultNoneflow_control: FlowControl— defaultFlowControl.NONExonxoff: bool— defaultFalsertscts: bool— defaultFalsedsrdtr: bool— defaultFalseinter_byte_timeout: float | None— defaultNone
SerialConfig.validate() raises ConfigError for invalid values (negative baudrate, negative timeouts, etc.).
Enumerate available serial ports. Returns PortInfo objects with:
device— device path (e.g./dev/ttyUSB0,COM3)name— short port namedescription— human-readable descriptionhwid— hardware ID stringvid,pid— USB vendor/product IDs (orNone)serial_number,manufacturer,product— USB metadata (orNone)
Parity—NONE,EVEN,ODD,MARK,SPACEStopBits—ONE,ONE_POINT_FIVE,TWOByteSize—FIVE,SIX,SEVEN,EIGHTFlowControl—NONE,HARDWARE,SOFTWARE
All exceptions inherit from SerialError:
SerialError— base exception for all serial port errorsPortNotFoundError— port not found or permission deniedConfigError— invalid configuration valueSerialTimeoutError— operation timed out
Print a table of available serial ports:
wireio-list-ports
# or
python -m wireio.tools.list_portsOutput example:
DEVICE DESCRIPTION HWID
-------------------------------------------------
/dev/ttyUSB0 USB Serial Device USB VID:PID=0403:6001
/dev/ttyS0 ttyS0 n/a
Interactive serial terminal:
wireio-miniterm /dev/ttyUSB0 115200
# or
python -m wireio.tools.miniterm /dev/ttyUSB0 115200 --echo --eol crlfOptions:
port— serial port device path (required)baudrate— baud rate (default:9600)--encoding— character encoding for display and input (default:utf-8)--echo— enable local echo of typed input--eol {cr,lf,crlf}— line ending appended to transmitted lines (default:crlf)
Press Ctrl+C to exit.
wireio is designed as a drop-in replacement for pyserial. Key differences:
| pyserial | wireio |
|---|---|
import serial |
from wireio import Serial |
serial.Serial(...) |
Serial(...) |
serial.tools.list_ports.comports() |
wireio.list_ports() |
serial.SerialException |
wireio.SerialError |
serial.serialutil.SerialTimeoutException |
wireio.SerialTimeoutError |
Separate pyserial-asyncio package |
Built-in AsyncSerial |
| No type hints | Full PEP 561 type hints |
| Python 2.7+ | Python 3.10+ |
# pyserial
import serial
ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=1)
ser.write(b'hello')
data = ser.read(100)
ser.close()
# wireio (same pattern works)
from wireio import Serial
ser = Serial('/dev/ttyUSB0', 9600, timeout=1)
ser.open()
ser.write(b'hello')
data = ser.read(100)
ser.close()
# wireio (preferred — context manager)
with Serial('/dev/ttyUSB0', baudrate=9600, timeout=1) as ser:
ser.write(b'hello')
data = ser.read(100)MIT