Summary
When a derived App class overrides on_mount() and calls await super().on_mount(), Textual dispatches the Mount event twice, causing the base class's on_mount() to execute twice.
Environment
- Textual version: 8.0.1
- Python version: 3.14.3
- Operating system: Windows 11 Enterprise (10.0.26200)
- Terminal: Windows Terminal
Expected Behavior
on_mount() should be called once per class in the MRO chain:
1. DerivedApp.on_mount() START
2. BaseApp.on_mount() (via await super().on_mount())
3. DerivedApp.on_mount() END
Actual Behavior
The base class's on_mount() is called twice:
1. DerivedApp.on_mount() START
2. BaseApp.on_mount() #1 (via await super().on_mount())
3. DerivedApp.on_mount() END
4. BaseApp.on_mount() #2 (Textual dispatches Mount again!)
Minimal Reproducer
#!/usr/bin/env python3
import asyncio
from textual.app import App, ComposeResult
from textual.widgets import Static
mount_count = 0
class BaseApp(App):
"""Base app with on_mount"""
def compose(self) -> ComposeResult:
yield Static("Test App")
async def on_mount(self) -> None:
global mount_count
mount_count += 1
print(f"BaseApp.on_mount() called (count: {mount_count})")
class DerivedApp(BaseApp):
"""Derived app that calls super().on_mount()"""
async def on_mount(self) -> None:
print("DerivedApp.on_mount() START")
await super().on_mount()
print("DerivedApp.on_mount() END")
async def main():
app = DerivedApp()
task = asyncio.create_task(app.run_async())
await asyncio.sleep(1.5)
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
print(f"\nResult: BaseApp.on_mount() was called {mount_count} time(s)")
# Expected: 1, Actual: 2
if __name__ == "__main__":
asyncio.run(main())
Output:
DerivedApp.on_mount() START
BaseApp.on_mount() called (count: 1)
DerivedApp.on_mount() END
BaseApp.on_mount() called (count: 2)
Result: BaseApp.on_mount() was called 2 time(s)
Investigation
I tested 6 different app configurations to isolate the trigger:
| Configuration |
Result |
| Simple App (no inheritance) |
✓ Single mount |
| App with mixins (no on_mount) |
✓ Single mount |
| App with mixins (with on_mount, no super) |
✓ Single mount |
| App with complex compose |
✓ Single mount |
| App with super().on_mount() |
✗ Double mount |
| Multiple mixins + super().on_mount() |
✗ Double mount |
The issue only occurs when:
- Using inheritance (base + derived)
- Derived class overrides
on_mount()
- Derived class calls
await super().on_mount()
Root Cause
From stack trace analysis, Textual dispatches events.Mount() from _process_messages() (app.py line 3365). When inheritance is involved, it appears to dispatch the event to both the derived and base classes separately, even though the derived class already calls the base via super().
Impact
- Performance: Wastes 2-3 seconds per app startup in complex apps
- Correctness: Can cause bugs if initialization isn't idempotent
- Developer confusion: Violates expected Python inheritance behavior
Workaround
Add an idempotency guard:
async def on_mount(self) -> None:
if hasattr(self, '_on_mount_completed'):
return # Skip duplicate call
# ... initialization code ...
self._on_mount_completed = True
Questions
- Is this intended Textual behavior?
- Should derived classes not call
super().on_mount()?
- What's the recommended pattern for inheritance with lifecycle hooks?
Additional Context
- Discovered in production app with 89K lines, 10 mixins
- Reproducible in minimal example above
- Affects all lifecycle hooks that use
super()
Related
Potentially related to #2914 and #3858
textual_bug_reproducer.py
Summary
When a derived
Appclass overrideson_mount()and callsawait super().on_mount(), Textual dispatches theMountevent twice, causing the base class'son_mount()to execute twice.Environment
Expected Behavior
on_mount()should be called once per class in the MRO chain:Actual Behavior
The base class's
on_mount()is called twice:Minimal Reproducer
Output:
Investigation
I tested 6 different app configurations to isolate the trigger:
The issue only occurs when:
on_mount()await super().on_mount()Root Cause
From stack trace analysis, Textual dispatches
events.Mount()from_process_messages()(app.py line 3365). When inheritance is involved, it appears to dispatch the event to both the derived and base classes separately, even though the derived class already calls the base viasuper().Impact
Workaround
Add an idempotency guard:
Questions
super().on_mount()?Additional Context
super()Related
Potentially related to #2914 and #3858
textual_bug_reproducer.py