-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Description
Textual's event loop runs create_task() tasks immediately, breaking libraries that expect deferred execution
The bug
When using asyncio.create_task() inside a Textual app, the created task can start executing immediately — before the calling coroutine continues. This differs from standard asyncio.run() behavior, where tasks typically don't run until the caller yields.
This causes compatibility issues with libraries like Telethon that rely on code executing after create_task() but before the task runs.
Minimal reproducible example
"""
Minimal reproduction of race condition in MTProtoSender when used with Textual.
"""
import logging
from telethon import TelegramClient
from textual.app import App
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
API_ID = 123
API_HASH = "hash"
class TestApp(App):
async def on_mount(self) -> None:
client = TelegramClient("test_race", API_ID, API_HASH)
try:
print("Connecting to Telegram...")
await client.connect()
print("connect() OK") # Hangs if bug present
me = await client.get_me()
print(f"get_me() returned: {me}")
finally:
await client.disconnect()
self.exit()
if __name__ == "__main__":
TestApp().run(headless=True)Expected output (both should match):
asyncio.run() order: ['before_create_task', 'after_create_task', 'task_started', 'after_sleep']
Textual order: ['before_create_task', 'after_create_task', 'task_started', 'after_sleep']
Actual output:
asyncio.run() order: ['before_create_task', 'after_create_task', 'task_started', 'after_sleep']
Textual order: ['before_create_task', 'task_started', 'after_create_task', 'after_sleep']
Notice in Textual, task_started appears before after_create_task — the task runs immediately after create_task().
Real-world impact
This breaks Telethon (Telegram client library). Their MTProtoSender.connect() does:
async def connect(self):
await self._connect() # creates send/recv loop tasks
self._user_connected = True # flag checked by the loops
async def _connect(self):
# ...
loop.create_task(self._send_loop())
loop.create_task(self._recv_loop())The loops check while self._user_connected. With Textual, they start running before the flag is set, see False, and exit immediately. RPC calls then hang forever.
Textual version: 6.7.1
Additional context
- Python's asyncio docs say tasks run "soon" after
create_task()— exact timing is implementation-defined - However, changing this behavior from what
asyncio.run()does breaks real-world libraries - This may be intentional for Textual's responsiveness, but it has compatibility implications