WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

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

@DoubleBoba

Description

@DoubleBoba

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions