Changelog¶
All notable changes to this project are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.2.0¶
Added¶
canonical_port_name(path, *, platform=None)returns one name for every spelling of a port, for use as a "same port?" key. On Windows it strips the\\.\/\\?\prefix and upper-cases (com8,\\.\COM8and\\?\COM8all giveCOM8, thePortInfo.devicediscovery reports); on POSIX it resolves symlinks when the path exists, so a/dev/serial/by-id/...link and its/dev/ttyUSB0target agree. Re-exported fromanyserial.anyserial.testing.faults_of(port)returns the liveFaultPlanof a port backed byMockBackend, such as either end ofserial_port_pair(), so tests can inject faults without reaching into the privateport._backend. RaisesTypeErrorfor other ports.- Windows:
SerialPort.port_infois populated.open_serial_portresolves the port through the SetupAPI discovery walk, in a worker thread before opening the handle; it was alwaysNoneon Windows.
Changed¶
find_serial_port(device=...)matches any name of the port by comparingcanonical_port_nameresults, instead of comparing strings verbatim:device="com8"findsCOM8on Windows and a by-id symlink finds its target on POSIX. Every verbatim match still matches.SerialPort.pathis documented as the path exactly as passed toopen_serial_port; it is not normalised.
Fixed¶
- Windows, Trio:
receive()andreceive_into()on an idle port no longer fail withSerialError("[WinError 1460] This operation returned because the timeout period expired") about 1 ms after the call. Trio raises theSTATUS_TIMEOUTthat ends an empty read under the wait-for-anyCOMMTIMEOUTSpolicy as an error; the Trio read path now treats it as the empty completion asyncio reports, and the read is reissued. - Windows, Trio:
WindowsBackend.wait_modem_event()passes theOVERLAPPEDto Trio by address. It previously passed the ctypes structure itself, which Trio cannot use as a dict key, so every pending wait failed withTypeErrorand leftWaitCommEventpending on buffers that were then released. AWaitCommEventthat completes synchronously now also waits for its completion packet. - Windows, asyncio: cancelling
WindowsBackend.wait_modem_event()no longer corrupts memory. The pendingWaitCommEventwas left running while its buffers and event handle were released, so the kernel wrote into freed memory when the event later fired oraclose()woke it, and the process crashed with an access violation. The wait is now cancelled withCancelIoExand its completion awaited before the buffers are released. ItsOVERLAPPEDalso no longer queues a completion packet to the proactor's port, which has no entry for it. - Windows:
open_serial_portleaves a path that already starts with\\?\unchanged, as it does for\\.\. It previously prepended\\.\, so\\?\COM8, and the\\?\…device-interface paths discovery reports for ports without aCOMnname, could not be opened. - Windows discovery reports
vid,pidandserial_numberfor FTDI adapters on FTDI's VCP driver, andserial_numberfor USB devices that have one. They are read from the device instance ID (FTDIBUS\VID_0403+PID_6001+<serial>A\0000,USB\VID_xxxx&PID_xxxx\<serial>); the hardware ID used before names no serial number and, for FTDI ports, is not in theUSB\VID_…form that was parsed. FTDI's appended port letter is dropped so the serial number matches Linux and macOS. The IDs Windows generates for devices without a serial number are not reported as serial numbers.
Documentation¶
- Windows: corrected the device-path rules.
open_serial_portadds the\\.\prefix itself, so"COM10"opens the port; the docs said it opened a file in the current directory.
0.1.2¶
Fixed¶
- Windows:
HandleWrappernow declares__weakref__in__slots__so it can be registered with the asyncioIocpProactor._registeredWeakSeton CPython >= 3.12. Without this slot everyopen_serial_portcall on the asyncio runtime path raisedTypeError: cannot create weak reference to 'HandleWrapper' objectat registration time, blocking every consumer that goes throughWindowsBackend.openon the asyncio loop.
0.1.1¶
Initial release.
Core¶
- Async-native serial transport built on AnyIO (>= 4.13).
- Immutable
SerialConfigwithwith_changes,FlowControl,RS485Config, andStrEnumtypes (ByteSize,Parity,StopBits). - Multi-inherited exception hierarchy compatible with stdlib and AnyIO bases.
- Tri-state
Capabilitymodel andSerialCapabilitiessnapshot per backend. BackendProtocol split intoSyncSerialBackend(POSIX) andAsyncSerialBackend(Windows).SerialPort,SerialConnectable, andopen_serial_portwith full AnyIO typed-attribute support.- Runtime reconfiguration via
port.configure(new_config)with serialized concurrent calls; failed applies leaveport.configunchanged. - Raw-bytes API:
receive,receive_available,receive_into,send,drain,drain_exact,input_waiting,output_waiting. MockBackendandFaultPlanunderanyserial.testingfor hardware-free unit testing, plusserial_port_pairhelper.- Blocking
anyserial.sync.SerialPortwrapper backed by a process-wideBlockingPortalProvider; per-calltimeout=keyword;configure_portal(backend=..., backend_options=...)for AnyIO backend selection.
Linux backend¶
LinuxBackendwith nonblocking fd I/O viaanyio.wait_readable/anyio.wait_writable, raw-mode termios, modem-line ioctls, BREAK, exclusive access viaflock, queue-depth, and buffer flush.- Standard and extended baud (
TCSETS2/BOTHER). ASYNC_LOW_LATENCYlow-latency mode with restore-on-close.- Kernel RS-485 (
TIOCSRS485) with read-modify-write that preserves driver-reserved bits and restores pre-touch state on close orconfigure(rs485=None). - Native sysfs-based port discovery with USB-ancestor resolution;
populates VID / PID / serial / manufacturer / product / location /
interface and emits a pyserial-compatible
hwidstring.
macOS (Darwin) backend¶
DarwinBackendwith custom baud viaIOSSIOSPEED, BREAK via the shared<sys/ttycom.h>numeric fallback, andUnsupportedPolicy- routed rejection oflow_latencyandrs485.- Native IOKit discovery walks
IOSerialBSDClient, prefers/dev/cu.*callout paths, and climbs the IORegistry parent chain for USB metadata. The ctypes facade over IOKit + CoreFoundation loads lazily so the module imports cleanly on Linux CI.
BSD backend¶
- One
BsdBackendfor FreeBSD / NetBSD / OpenBSD / DragonFly. Custom baud via integerc_ispeed/c_ospeedpassthrough;low_latency/rs485rejected viaUnsupportedPolicy. /dev-scan discovery with per-variant glob sets. USB metadata is intentionally not populated — uselist_serial_ports(backend="pyserial")when VID / PID is needed.
Windows backend¶
WindowsBackendimplementsAsyncSerialBackendand dispatches hot-pathreceive/sendthrough each runtime's native IOCP machinery:- Trio →
trio.lowlevel.register_with_iocp+readinto_overlapped/write_overlapped. - asyncio on
ProactorEventLoop→loop._proactor._register+_overlapped.Overlapped.ReadFileInto/WriteFile(zero-copy via CPython 3.12+ReadFileInto). - No worker-thread fallback.
SelectorEventLoopraisesUnsupportedPlatformErrorat open time pointing atWindowsProactorEventLoopPolicy. - SetupAPI port discovery via
GUID_DEVINTERFACE_COMPORTpopulates VID / PID / serial / manufacturer / product / location on USB-attached adapters;hwidis pyserial-compatible. Falls back toHKLM\HARDWARE\DEVICEMAP\SERIALCOMMviawinregwhen SetupAPI enumeration fails. WaitCommEventmodem-line change notification (EV_CTS | EV_DSR | EV_RING | EV_RLSD | EV_ERR | EV_BREAK).- DCB round-trip (
GetCommState→ overlay →SetCommState) preserves vendor state stored in reserved DCB fields by FTDI / Prolific / CH340 drivers. - "Wait-for-any"
COMMTIMEOUTSpolicy (MAXDWORD / MAXDWORD / 1 ms) with internal retry loop on zero-byte completions, so idlereceive()doesn't surface spurious EOF. - Win32 error translation (
ERROR_FILE_NOT_FOUND→PortNotFoundError,ERROR_ACCESS_DENIED/ERROR_SHARING_VIOLATION→PortBusyError,ERROR_INVALID_HANDLE/ERROR_OPERATION_ABORTED→SerialClosedError,ERROR_INVALID_PARAMETERon config →UnsupportedConfigurationError,ERROR_DEVICE_REMOVED/ERROR_NOT_READY/ERROR_GEN_FAILURE→SerialDisconnectedError). Exceptions carry a.winerrorattribute. - Capability snapshot reports
SUPPORTEDfor every feature exceptlow_latency(no Windows equivalent ofASYNC_LOW_LATENCY) andrs485(FTDI VCP RS-485 is driver config, not a runtime API).
Discovery¶
list_serial_ports(),find_serial_port(...), and thePortInfodata model — async, always-live, no caching.- Optional
pyudev(Linux) andpyserial(cross-platform) backends selectable via thebackend=keyword. Each raisesImportErrorwith the exact install command when the extra isn't installed. port.port_infotyped attribute onSerialPort:open_serial_portresolves the device path through native discovery and exposes the result onport.port_infoand via the typed-attribute interface.
Tooling and CI¶
pyproject.tomlwithhatchling+hatch-vcsbuild, AnyIO >= 4.13 runtime dep, Python 3.13 / 3.14 support.- uv-managed dependency groups (lint, type, test, docs, bench).
- Ruff, mypy, pyright, pytest, coverage, pre-commit configuration.
- GitHub Actions CI for lint, typecheck, and tests on Linux, macOS,
and Windows across Python 3.13 / 3.14 × asyncio / asyncio + uvloop /
trio (Windows: asyncio Proactor / trio). FreeBSD smoke job via
cross-platform-actions. - Hardware test marker default-deselected (
pytest -m "not hardware"); opt-in viapytest -m hardwarewithANYSERIAL_TEST_PORT. - Benchmark suite under
benchmarks/: receive / send latency, throughput, many-port fan-out, allocation profile, sync-vs-async, Windows IOCP scenarios over an opt-in configured serial pair, and apyserial-asynciohead-to-head. Nightly bench workflow records baselines per backend.
Documentation¶
- Full documentation site covering quickstart, configuration, capabilities, discovery, runtime reconfiguration, RS-485, AnyIO backend selection, uvloop, cancellation, performance, sync wrapper, hardware testing, troubleshooting, migration from pySerial, and per-platform pages (Linux tuning, macOS, BSD, Windows).
- MIT license.