Textual's event loop runs `create_task()` tasks immediately, breaking libraries that expect deferred execution
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
Thank you for your issue. Give us a little time to review it.
PS. You might want to check the FAQ if you haven't done so already.
This project is developed and maintained by Will McGugan. Consider sponsoring Will's work on this project (and others).
This is an automated reply, generated by FAQtory
Here you may see another issues i created to telethon's issue tracker. It may contain other important details. https://github.com/LonamiWebs/Telethon/issues/4721