Skip to main content

OK serial I/O for Python   🔌〡〇〡〇〡🐍

A Python serial port library (based on PySerial) with improved port discovery and I/O semantics. (API reference)

Think twice before using this library! Consider something more established:

  • good old PySerial - the implementation under ok-serial, established, widely used
  • pyserial-asyncio - official and "proper" asyncio support for PySerial
  • pyserial-asyncio-fast - pyserial-asyncio fork designed for faster writes
  • aioserial - alternative asyncio wrapper designed for ease of use
  • bonus recommendation: tio - not a library, not Python, but a great serial terminal utility

Also see my own ok-serial-terminal, a terminal program based on this library.

Purpose

Since 2001, PySerial has been the workhorse serial port / UART library for Python. It runs on lots of platforms and abstracts gnarly system details. But, some issues keep coming up:

  • USB serial ports get temporary names like /dev/ttyACM3 or COM9. PySerial's serial.tools.list_ports.grep(...), OS-level /dev/serial/by-id paths, and custom udev rules are helpful but clunky.

  • Nonblocking or concurrent PySerial I/O is tricky and often broken entirely.

  • PySerial has small buffers; overruns lose data and/or block unexpectedly.

  • PySerial doesn't lock ports by default, and only supports one advisory locking method. Bad things happen when multiple programs try to use the same port.

The ok-serial library uses PySerial internally but has a revised interface:

  • Ports are referenced by match strings with wildcard support (eg. RP2040 or 2e43:0226) or, for by arbitrary PortInfo -> bool callables.

  • I/O operations are thread safe and can be blocking, non-blocking, timeout-based, or async. Blocking operations can be cleanly interrupted. The semantics of concurrent access, partial reads/writes, interruption, I/O errors, closure, etc. are all well defined.

  • I/O buffers are limited only by memory; writes never block. (A blocking drain is available.)

  • Several port locking modes are supported, with exclusive locking by default. All of /var/lock/LCK..* files, flock(...) (like PySerial), and TIOCEXCL are used (if available).

  • SerialConnectionMonitor is an automatic reconnection helper for graceful handling of pluggable devices.

Installation

pip install ok-serial

(or uv add ok-serial, etc.)

Usage

Here is a minimal example:

import ok_serial

conn = ok_serial.SerialConnection(match="MyDevice", baud=115200)
conn.write(b"Hello Device!")
while (data := conn.read_sync(timeout=5)):
    print("Received data:", data)
print("...5 seconds elapsed with no data")

(Note that "MyDevice" is a port match expression.)

API elements worth knowing include:

I/O methods come in different flavors:

See the full API reference docs for interface details.

Unless called out in the docs, all methods and functions are thread-safe: any method may be called from any thread at any time, and *_async methods may be awaited from any event loop in any thread. Any error or closure on a connection interrupts all operations on that connection.

Serial port attributes

Serial ports have metadata attributes like descriptive text, USB vendor/product ID, serial number and the like. These are captured as key/value pairs in PortInfo.attr and returned by scan_serial_ports.

Attributes come from PySerial and are platform dependent but typically include:

  • device - system device name, eg. /dev/ttyUSB1 or COM3
  • description - human readable text, eg. Arduino Uno
  • manufacturer - USB device manufacturer name, eg. `
  • vid_pid - USB vendor and product ID, eg. 0403:6001
  • serial_number - USB device serial, eg. DF62585783553434
  • location - system bus attachment path, eg. 3-2.1:1.0

To see all the attributes, install ok-serial and run okserial -v:

Port: /dev/ttyACM3 Kq2p 3:12s
  device=/dev/ttyACM3
  name=ttyACM3
  description='Feather RP2040 RFM - Pico Serial'
  hwid='USB VID:PID=239A:812D SER=DF62585783553434 LOCATION=3-2.1:1.0'
  vid=9114
  pid=33069
  serial_number=DF62585783553434
  location=3-2.1:1.0
  manufacturer=Adafruit
  product='Feather RP2040 RFM'
  interface='Pico Serial'
  usb_device_path=/sys/devices/pci0000:00/0000:00:14.0/usb3/3-2/3-2.1
  device_path=/sys/devices/pci0000:00/0000:00:14.0/usb3/3-2/3-2.1/3-2.1:1.0
  subsystem=usb
  usb_interface_path=/sys/devices/pci0000:00/0000:00:14.0/usb3/3-2/3-2.1/3-2.1:1.0
  tid=Kq2p
  time=2026-08-04T12:38:39.800
  vid_pid=239a:812d
...

Port matching

SerialConnection(match=...) and SerialConnectionMonitor(...) take either a match string or a predicate callable (PortInfo -> bool).

A match string is split on whitespace into glob tokens. Each token must appear as a whole-word case-insensitive glob (with * and ? wildcards) in some attribute value:

  • Pico - some attribute contains the word pico (any case)
  • RP2040 DF625* - some attribute contains rp2040 AND some attribute contains a word starting with df625
  • 2e8a:0005 - matches the canonical vid_pid form (lowercase hex)
  • ttyS1 - DOES match /dev/ttyS1, does NOT match /dev/ttyS10

Word boundaries treat any non-alphanumeric character (/, :, _, etc.) as a separator, so partial USB IDs and device-path fragments work naturally.

For anything more elaborate (substring matching across attribute boundaries, regex, negation, etc.), do your own filtering, or pass a callable:

ok_serial.SerialConnectionMonitor(
    match=lambda p: p.attr.get("manufacturer") == "Adafruit"
    and p.attr.get("serial_number", "").startswith("DF625"),
)

Sharing modes

When opening a port, SerialConnection offers a choice of sharing modes:

  • oblivious (not recommended) - Checks no locks and holds no locks. The port may be opened concurrently, leading to corruption.
  • polite - Checks for locks before opening the port, but holds no locks while running. Abandons the port if another process is detected using it.
  • exclusive (the default) - Checks for locks before opening the port, and holds locks to guard against other uses of the port.
  • stomp (use with care!) - Any other process using the port is killed, if possible; locks are held, if possible; the port is opened regardless.

Sharing modes are limited by OS capabilities, process permissions, and the conventions of port usage coordination. Best efforts are taken but your mileage may vary.

Command line utility

Installing the ok-serial package installs the okserial utility, which lists serial ports:

$ okserial
🔎 Finding serial ports...
✅ 2 serial ports found
/dev/ttyACM0 ZdvG usb 0424:494c 'USB2 Controller Hub - UART Bridge' 1d+04:18:11s
/dev/ttyACM3 Kq2p usb 239a:812d 'Feather RP2040 RFM' DF62585783553434 3:12s

Each line includes the device name, tio-compatible topology ID, and whichever of the subsystem, USB vendor/product ID, description, and serial number are known, along with the age of the port.

Run okserial -v print extra detail; see okserial --help for more options.

For an interactive terminal, see ok-serial-terminal.

Socat for testing and profit

On Unix-ish systems, socat is handy for connecting serial-port apps (using ok-serial or otherwise) to other endpoints (Unix programs, TCP sockets, other serial-port apps, etc). Install it with your favorite package manager (eg. sudo apt install socat), and run something like this in one window:

socat pty,raw,echo=0,link=socat.tmp exec:$SHELL,pty,stderr,setsid,ctty

The first socat argument pty,raw,echo=0,link=socat.tmp allocates a pseudoterminal (pty) that looks like a serial port, and creates a ./socat.tmp symlink. The ,raw,echo=0 suppresses default pty echo behavior to avoid the shell looping on its own output.

The second socat argument starts a shell on its own pty, but this could be any socat endpoint (exec:cat, tcp:localhost:8000, another pty,..., etc).

Socat will shuffle data between the two. Try this Python in another window in the same directory:

import ok_serial
import time
with ok_serial.SerialConnection(port="socat.run.tmp") as conn:
    conn.write(b"echo Hello World\n")
    time.sleep(0.5)  # let shell respond
    print(conn.read_sync())

You should see the echo Hello World echoed, then Hello World, then the next shell prompt. (Use ok-serial-terminal to connect and operate the shell interactively.)

Metadata

Release files for ok-serial 0.9

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ok-serial 0.9
File Size Uploaded Provenance Actions
ok_serial-0.9.tar.gz 22.0 kB Without provenance Details for ok_serial-0.9.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for ok-serial 0.9
File Interpreter ABI Platform Actions
ok_serial-0.9-py3-none-any.whl Python 3 none any Details for ok_serial-0.9-py3-none-any.whl

Total release size: 49.1 kB

Release files / ok_serial-0.9.tar.gz

Download URL ok_serial-0.9.tar.gz
Size 22.0 kB
Tags Source
SHA-256 checksum
How to use checksums
ba2fbef6062bf8a0361fee9d3525e9db4c7b4863d797812c6221c1066c9fad16
BLAKE2b-256 checksum
How to use checksums
bccaca3485a0456288e2c2660a817e7f0ed533b2e0fdc5253242982344c989c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / ok_serial-0.9-py3-none-any.whl

Download URL ok_serial-0.9-py3-none-any.whl
Size 27.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
763cef23a8c6dc67d773114cc465dbccbb88e6740bffaad59b4b5da523e7e20e
BLAKE2b-256 checksum
How to use checksums
100f679b387062f873f5db9877ccc8df8ee6e807aa20c84b70cd0395f66604d9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for ok-serial 0.9

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page