Skip to content

[BUG] RecursionError when stacking translucent Screens with Header(show_clock=True) #6340

@kareemalzahal03

Description

@kareemalzahal03

Summary

I've encountered a reproducible RecursionError: maximum recursion depth exceeded when pushing a new Screen on top of another translucent Screen that both contain a Header(show_clock=True).

This issue occurs in a minimal example and does not appear to be caused by application logic. Small changes such as disabling the clock or removing screen translucency eliminate the crash (according to my testing).

FYI: This bug does occur in the latest version (textual==7.4.0), but this bug was first discovered while using textual==7.3.0.

Let me know if you'd like me to further reduce this example or test a proposed fix. Happy to help debug further.

Minimal Reproducible Example

from textual.app import App, ComposeResult
from textual.widgets import Button, Label, Footer, Header, DirectoryTree
from textual.screen import Screen

# How the app works:
# A simple screen that allows you to select a file in the current directory.
# Press 'n' to open the current directory and choose a file. Press ESC to return to main screen.
# When you click or press enter on a file, a popup appears asking you to confirm your choice.
# Select 'Cancel' to go back to the directory tree and choose another choice or cancel.
# Select 'Confirm' to go back to the main screen and see "File Selected: {file}"

# How to reproduce the bug:
# 1. Open the app
# 2. Press 'n' to open file directory
# 3. Click a file to select it (or press enter)
# 4. A screen will appear asking you to confirm your choice <- Where the app crashes
# 5. Confirm your choice, and you will return to the main screen

# If the app doesn't crash on the first try, try loading another file or restarting.
# Eventually, when prompted to confirm, the program should crash with the following message:
# RecursionError: maximum recursion depth exceeded

# Applying ANY of the following changes appears to FIX the bug (what I could find):
# - Initializing either of 'ChooseFile' or 'MainScreen' Header widget with show_clock = False
# - Changing the 'Screen' CSS opacity to 100%


class ConfirmScreen(Screen[bool]):  
    """Screen with a dialog to confirm or cancel."""

    def compose(self) -> ComposeResult:
        yield Label("Are you sure?")
        yield Button("Confirm", variant="error")
        yield Button("Cancel", variant="primary")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        """Dismisses screen with choice (confirm or cancel)."""
        self.dismiss(event.button.variant == "error")


class ChooseFile(Screen[str]):
    """A screen with a directory tree. User chooses a file and confirms choice."""
        
    def compose(self) -> ComposeResult:
        yield Header(show_clock = True) # Change show_clock = False to fix bug!
        yield DirectoryTree("./")
        yield Footer()
        
    def on_mount(self) -> None:
        self.title = "Select File to Load"
        self.query_one(DirectoryTree).focus()

    def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
        """When a file is selected, confirm with the user then dismiss with file's path"""

        def _confirm(confirmed: bool | None) -> None:
            if confirmed is not None and confirmed:
                self.dismiss(str(event.path))

        self.app.push_screen(ConfirmScreen(), _confirm)

    BINDINGS = [("escape", "cancel", "Cancel")]

    def action_cancel(self) -> None:
        """Cancel the request to choose a new file."""
        self.dismiss(None)


class MainScreen(Screen):

    def compose(self) -> ComposeResult:
        yield Header(show_clock = True) # Change show_clock = False to fix bug!
        yield Label("No file selected (Press n)")
        yield Footer()

    BINDINGS = [("n", "load_new_file", "Load New File")]

    def action_load_new_file(self):
        """Prompt user to choose a new file, and print choice to the label."""

        def _choose_file(file: str | None) -> None:
            if file is not None:
                self.query_one(Label).content = f"File Selected: {file}"

        self.app.push_screen(screen = ChooseFile(), callback = _choose_file)


class MRE(App[None]):

    CSS = """
    Screen {
        background: $background 50%; # Change opacity to 100% to fix bug!
    }
    """

    def on_mount(self) -> None:
        self.push_screen(MainScreen())
        

if __name__ == "__main__":
    MRE().run()

Screenshot

Image

Textual Diagnostics

Versions

Name Value
Textual 7.4.0
Rich 14.2.0

Python

Name Value
Version 3.13.11
Implementation CPython
Compiler Clang 17.0.0 (clang-1700.4.4.1)
Executable /Users/kareemalzahal/Desktop/AIN/AlfalfaOptim/.venv/bin/python3.13

Operating System

Name Value
System Darwin
Release 25.2.0
Version Darwin Kernel Version 25.2.0: Tue Nov 18 21:09:55 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T8103

Terminal

Name Value
Terminal Application Apple_Terminal (466)
TERM xterm-256color
COLORTERM truecolor
FORCE_COLOR Not set
NO_COLOR Not set

Rich Console options

Name Value
size width=95, height=40
legacy_windows False
min_width 1
max_width 95
is_terminal True
encoding utf-8
max_height 40
justify None
overflow None
no_wrap False
highlight None
markup None
height None

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