textual icon indicating copy to clipboard operation
textual copied to clipboard

Textual's event loop runs `create_task()` tasks immediately, breaking libraries that expect deferred execution

Open DoubleBoba opened this issue 1 month ago • 2 comments

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

DoubleBoba avatar Dec 07 '25 15:12 DoubleBoba

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

github-actions[bot] avatar Dec 07 '25 15:12 github-actions[bot]

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

DoubleBoba avatar Dec 07 '25 15:12 DoubleBoba