Question: I'm learning python and asyncio and after having success with asyncio for a TCP client/server I took my first stab at creating a serial client/server using pyserial-asyncio running in bash on a Raspberry Pi 5 using Python 3.8 (I cannot change version). Here is the server: import asyncio import serial_asyncio class UARTProtocol(asyncio.Protocol): def __init__(self): self.transport = None def connection_made(self, transport): self.transport = transport print('Port opened', transport) def data_received(self, data): print('Data received:', data.decode()) # Echo received data back (example) self.transport.write(data) # Close the connection if 'exit' is received if data == b"exit\r": self.transport.close() def connection_lost(self, exc): print('Port closed') self.transport = None def pause_writing(self): print('pause writing') print(self.transport.get_write_buffer_size()) def resume_writing(self): print(self.transport.get_write_buffer_size()) print('resume writing') async def run_uart_server(): loop = asyncio.get_running_loop() try: transport, protocol = await serial_asyncio.create_serial_connection(loop, UARTProtocol, '/dev/ttyAMA2', baudrate=9600) print("UART server started.") await asyncio.Future() # Run forever except serial.serialutil.SerialException as e: print(f"Error: Could not open serial port: {e}") finally: if transport: transport.close() if __name__ == "__main__": asyncio.run(run_uart_server())  and the client: import asyncio import serial_asyncio async def uart_client(port, baudrate): try: reader, writer = await serial_asyncio.open_serial_connection(url=port, baudrate=baudrate) print(f"Connected to {port} at {baudrate} bps") async def receive_data(): while True: try: data = await reader.readline() if data: print(f"Received: {data.decode().strip()}") except Exception as e: print(f"Error reading data: {e}") break async def send_data(): while True: message = input("Enter message to send (or 'exit' to quit): ") if message.lower() == 'exit': break writer.write((message + '\n').encode()) # writer.write_eof() await writer.drain() print(f"Sent: {message}") await asyncio.gather(receive_data(), send_data()) except serial.SerialException as e: print(f"Error opening serial port: {e}") finally: if 'writer' in locals(): writer.close() await writer.wait_closed() print("Connection closed.") if __name__ == "__main__": asyncio.run(uart_client('/dev/ttyAMA1', 9600))  I want the client to prompt me for some text which is immediately sent to the server and printed there. I can get the client to prompt me for text, but the server doesn't display any of it until after I type exit in the client to close the connection and then it prints all of the text I typed in the client loop. Among many other things, I've tried adding writer.write_eof() in the client (see commented out line in the client code below) and that succeeds in the server immediately displaying the preceding text from the client but then the client never prompts me for input again. If I run the server and just do echo foo > /dev/ttyAMA1 from bash the server prints foo immediately so I suspect the client is the problem. What am I doing wrong?

Answer: The problem here is that input is a blocking call. We all know that input doesn't return until the user types some text and hits the enter key. We also know that it's not an async function, therefore it doesn't use the event loop. So it can't run other Tasks while it's waiting for the user to type something. All the asyncio Tasks are frozen until the user does something. All functions that aren't async have this same quality. Most functions do not wait for the user, and most of them return fairly quickly. It's not normally a big deal. But to use input in an asyncio program requires a little work. The solution is to issue the call to input in another thread. That thread will be blocked, but the main thread will keep going. All the other async Tasks will keep running, except for the one that's waiting for the user. The convenient way to do this is with the loop.run_in_executor(), which was available before Python3.8. Here is a little script to demonstrate its use. I tested this with 3.13 but I avoided using any feature introduced after 3.8. Or at least I hope so. import sys import asyncio async def ticks(): while True: await asyncio.sleep(1.0) sys.stdout.write(".") sys.stdout.flush() async def ask(): def inp(): return input("Command?") while True: loop = asyncio.get_event_loop() x = await loop.run_in_executor(None, inp) print(x) if x == 'stop': break async def main(): tix = asyncio.create_task(ticks()) await ask() tix.cancel() if __name__ == "__main__": asyncio.run(main())