Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save aont/5f3c46b6fed5aa7e627d94654cbc3256 to your computer and use it in GitHub Desktop.

Select an option

Save aont/5f3c46b6fed5aa7e627d94654cbc3256 to your computer and use it in GitHub Desktop.

Reading from stdin with asyncio (and writing to stderr)

This short Python script shows how to use asyncio to read a single line from standard input (stdin) asynchronously and report what was read to standard error (stderr). It’s a compact example of wiring low-level OS pipes into asyncio’s high-level stream APIs.

What the code does

  1. Creates an asynchronous reader from a file-like object make_reader(fp) takes a binary file object (like sys.stdin.buffer) and:

    • Gets the current event loop.
    • Builds an asyncio.StreamReader and its StreamReaderProtocol.
    • Connects the OS read pipe (fp) to that protocol with loop.connect_read_pipe.
    • Returns the StreamReader so you can await reader.readline() and similar.
  2. Creates an asynchronous writer from a file-like object make_writer(fp):

    • Gets the current event loop.
    • Uses loop.connect_write_pipe to attach the OS write pipe (fp) to an asyncio transport using FlowControlMixin.
    • Wraps the transport in an asyncio.StreamWriter so you can call writer.write(...) and await writer.drain().
  3. Main flow In main():

    • It builds stdin via make_reader(sys.stdin.buffer).
    • It builds stderr via make_writer(sys.stderr.buffer). (The stdout setup is shown but commented out.)
    • It awaits one line from stdin: line = await stdin.readline().
    • It writes a debug message like line=b'hello\n' to stderr, then drains to ensure the bytes are flushed.
  4. Runs the coroutine asyncio.run(main()) starts the event loop, executes main(), and closes the loop cleanly.

Why this is useful

  • Non-blocking I/O: In CLI tools that must remain responsive—maybe they also handle sockets, timers, or subprocesses—you don’t want a blocking .readline() call. Using asyncio lets your program do other work while waiting for input.
  • Interfacing with UNIX pipes: The connect_read_pipe / connect_write_pipe functions bridge low-level file descriptors (like stdin/stdout/stderr or any pipe) to asyncio streams.

Notes and caveats

  • Binary mode: The code uses .buffer, so you read and write bytes, not strings. If you need text, decode/encode or wrap with asyncio.StreamReader.readline() followed by .decode().
  • Single read: As written, it reads exactly one line and exits. To process multiple lines, loop over await stdin.readline() until you get b'' (EOF).
  • Flow control: Using await writer.drain() yields to the event loop if the pipe’s buffer is full, preventing unbounded memory growth.

That would read a line from stdin, print a debug representation to stderr, and echo the raw line to stdout—entirely using asyncio.

import asyncio
import sys
import typing
async def make_reader(fp: typing.BinaryIO):
loop = asyncio.get_running_loop()
reader = asyncio.StreamReader()
protocol = asyncio.StreamReaderProtocol(reader)
transport, _ = await loop.connect_read_pipe(lambda: protocol, fp)
return reader
async def make_writer(fp: typing.BinaryIO):
loop = asyncio.get_running_loop()
transport, protocol = await loop.connect_write_pipe(
asyncio.streams.FlowControlMixin, fp
)
return asyncio.StreamWriter(transport, protocol, None, loop)
async def main():
reader = await make_reader(sys.stdin.buffer)
writer = await make_writer(sys.stderr.buffer)
line = await reader.readline()
writer.write(f"{line=}\n".encode())
await writer.drain()
asyncio.run(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment