Skip to main content

anyserial

Async-native serial I/O for Python, built on AnyIO.

CI PyPI version Python versions License: MIT

Overview

anyserial is a ground-up async serial transport. It runs on top of AnyIO, so the same code works under asyncio, uvloop, or trio without changes, and it exposes a thin blocking wrapper for scripts and test benches that don't want an event loop.

The focus is low-latency, predictable I/O against real hardware:

  • Async-native. Readiness-driven I/O on nonblocking file descriptors — no worker threads on the POSIX hot path.
  • POSIX first-class. Linux, macOS, and BSD; Windows via IOCP (trio and ProactorEventLoop).
  • Explicit capabilities. Features that a given platform or adapter can't support fail loudly at configure time — no silent emulation.
  • Raw bytes only. Framing belongs in user code or downstream libraries; compose with anyio.streams.buffered.BufferedByteStream for line- or delimiter-based reads.
  • Immutable, typed config. Frozen dataclasses, strict type checking, full PEP 561 support.
  • Runtime reconfiguration. Change baud, parity, or flow control on an open port without reopening.
  • RS-485, low-latency mode, custom baud rates where the platform exposes them.

Requirements

  • Python 3.13 or 3.14
  • Linux, macOS, BSD, or Windows
  • anyio >= 4.13

Installation

uv add anyserial
# or
pip install anyserial

Optional extras:

uv add "anyserial[uvloop]"              # Linux/macOS uvloop event loop
uv add "anyserial[winloop]"             # Windows winloop event loop
uv add "anyserial[trio]"                # trio runtime
uv add "anyserial[discovery-pyudev]"    # richer Linux port discovery
uv add "anyserial[discovery-pyserial]"  # pyserial-based discovery fallback

Usage

Async

import anyio
from anyserial import SerialConfig, open_serial_port


async def main() -> None:
    config = SerialConfig(baudrate=115_200)
    async with await open_serial_port("/dev/ttyUSB0", config) as port:
        await port.send(b"AT\r\n")
        reply = await port.receive(64)
        print(reply)


anyio.run(main)

receive(max_bytes) returns as soon as any bytes are available; a clean EOF raises SerialDisconnectedError. send handles partial writes internally. Use an AnyIO cancel scope to bound a read:

with anyio.move_on_after(1.0):
    reply = await port.receive(64)

Sync

from anyserial.sync import SerialPort

with SerialPort.open("/dev/ttyUSB0", baudrate=115_200) as port:
    port.send(b"ping\n")
    reply = port.receive(1024, timeout=1.0)

The sync wrapper is backed by a process-wide anyio.from_thread.BlockingPortalProvider; every blocking call accepts an optional timeout=. Each call pays a one-time portal hop (~tens to hundreds of µs on a modern laptop) — fine for setup and occasional I/O, visible on tight request/response loops. Prefer async for those; see docs/sync.md.

Line-framed protocols

For protocols terminated by \n, \r, or any fixed delimiter, wrap the port in AnyIO's BufferedByteStream. It handles partial reads across the delimiter for you, delegates send to the underlying port, and has no measurable overhead versus a hand-rolled loop:

from anyio.streams.buffered import BufferedByteStream

async with await open_serial_port("/dev/ttyUSB0", config) as port:
    buffered = BufferedByteStream(port)
    await buffered.send(b"AT\r")
    line = await buffered.receive_until(b"\r", max_bytes=512)

Fan-out: reading from many devices at once

One event loop handles N ports concurrently, no thread-per-port. This is where anyserial pulls ahead of sync libraries — see the hardware case study for numbers (6× faster than thread-per-port pyserial at N=16 on pty-backed peers).

import anyio
from anyserial import SerialConfig, open_serial_port


async def poll_one(path: str, results: dict[str, bytes]) -> None:
    async with await open_serial_port(path, SerialConfig(baudrate=115_200)) as port:
        await port.send(b"A\r")
        results[path] = await port.receive(256)


async def main() -> None:
    paths = ["/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2"]
    results: dict[str, bytes] = {}
    async with anyio.create_task_group() as tg:
        for p in paths:
            tg.start_soon(poll_one, p, results)
    for path, frame in results.items():
        print(path, frame)


anyio.run(main)

Discovery

from anyserial import find_serial_port, list_serial_ports

for info in await list_serial_ports():
    print(info.device, info.vid, info.pid, info.serial_number)

ftdi = await find_serial_port(vid=0x0403, pid=0x6001)

Testing without hardware

from anyserial.testing import serial_port_pair

a, b = serial_port_pair()
await a.send(b"hello")
assert await b.receive(5) == b"hello"

faults_of(port) returns a side's live FaultPlan for fault injection (faults_of(b).disconnected = True); MockBackend and FaultPlan are in anyserial.testing too.

Documentation

Full documentation lives at https://anyserial.graysonbellamy.dev/. Starting points:

Contributing

Issues and PRs are welcome. To get a local checkout running:

git clone https://github.com/GraysonBellamy/anyserial
cd anyserial
uv sync --all-extras
uv run pre-commit install

Before opening a PR:

uv run pytest
uv run ruff check
uv run ruff format --check
uv run mypy

Hardware-dependent tests are opt-in via pytest -m hardware with ANYSERIAL_TEST_PORT set; see docs/hardware-testing.md.

License

MIT. See LICENSE.

Metadata

Release files for anyserial 0.2.0

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

Source distribution (sdist)

Source distribution for anyserial 0.2.0
File Size Uploaded
anyserial-0.2.0.tar.gz 248.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for anyserial 0.2.0
File Interpreter ABI Platform
anyserial-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 384.9 kB

Release files / anyserial-0.2.0.tar.gz

Download URL anyserial-0.2.0.tar.gz
Size 248.4 kB
Tags Source
SHA-256 checksum
How to use checksums
c98c5c46f8b847e92e2c48eab21b026b5725d6e4ce3001291168a38f4202baf8
BLAKE2b-256 checksum
How to use checksums
25434de5a5dd1a63a90865ebbbbf5aad926fa5127d64ff9e9acac296a434518b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / anyserial-0.2.0-py3-none-any.whl

Download URL anyserial-0.2.0-py3-none-any.whl
Size 136.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e3d24a5873b68948c085351f010aa9a00f3b9f31559da0ea515fe5ab00df5d1d
BLAKE2b-256 checksum
How to use checksums
eb349276d50e7e146eba3e5ef747d4c16cee22f52fa233608a90c050ede93e34
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

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