Textualize/textual · error · DuplicateKeyHandlers

Multiple handlers for key press

Error message

Multiple handlers for key press {key_name!r}.\nWe found both {first_handler!r} and {second_handler!r}, and didn't know which to call.\nConsider combining them into a single handler.

What it means

When a key is pressed, Textual's dispatch_key scans the widget for handler methods (key_<key>, key_<unicode>) and also checks the декорated @on(Key) style handlers via metadata. If it finds two candidate handlers for the same key, it refuses to guess and raises DuplicateKeyHandlers, suggesting you merge them.

Solutions

  1. Delete or rename one of the duplicate handlers so a single method handles the key
  2. If migrating to @on decorators, remove the old key_<name> method
  3. Move key handling to a parent/child level instead of duplicating on the same widget
  4. Merge both handlers' logic into one method

Example fix

# before
def key_a(self) -> None:
    self.add_note('a')

@on(Key)
def key_handler(self, event: Key) -> None:
    if event.key == 'a':
        self.add_note('a')  # duplicate

# after
@on(Key)
def key_handler(self, event: Key) -> None:
    if event.key == 'a':
        self.add_note('a')
Defensive patterns

Strategy: validation

Validate before calling

import inspect

def handler_names(widget, key: str) -> list[str]:
    names = []
    for name, member in inspect.getmembers(type(widget), inspect.isfunction):
        if name == f'key_{key}':
            names.append(name)
        for meta in getattr(member, '__textual_on', []):
            if getattr(meta[1] if isinstance(meta, tuple) else meta, 'key', None) == key:
                names.append(name)
    return names

# assert len(handler_names(self, 'a')) <= 1

Type guard

def has_single_key_handler(widget, key: str) -> bool:
    return len(handler_names(widget, key)) == 1

Try / catch

from textual._dispatch_key import DuplicateKeyHandlers
try:
    dispatch_key(widget, key_event)
except DuplicateKeyHandlers:
    widget.notify('ambiguous key handling; check logs')

Prevention

When it happens

Trigger: Defining both key_a(self) and an @on(Key('a')) handler on the same widget, or two decorated handlers matching the same key event, then pressing that key. Also occurs when a subclass inherits a key_x method and adds another handler for the same key.

Common situations: Copy-pasting a handler and changing only the decorator, migrating from key_ methods to @on decorators without removing the old method, or inheriting widgets whose base already defines a handler for the same key.

Related errors


AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27). Data as JSON: /api/errors/a1769ea784f6d9c8. Report an issue: GitHub.

Appendix: source

Thrown at src/textual/_dispatch_key.py:43

    Raises:
        DuplicateKeyHandlers: When there's more than 1 handler that could handle this key.
    """

    def get_key_handler(pump: MessagePump, key: str) -> Callable | None:
        """Look for the public and private handler methods by name on self."""
        return getattr(pump, f"key_{key}", None) or getattr(pump, f"_key_{key}", None)

    handled = False
    invoked_method = None
    key_name = event.name
    if not key_name:
        return False

    def _raise_duplicate_key_handlers_error(
        key_name: str, first_handler: str, second_handler: str
    ) -> None:
        """Raise exception for case where user presses a key and there are multiple candidate key handler methods for it."""
        raise DuplicateKeyHandlers(
            f"Multiple handlers for key press {key_name!r}.\n"
            f"We found both {first_handler!r} and {second_handler!r}, "
            f"and didn't know which to call.\n"
            f"Consider combining them into a single handler.",
        )

    try:
        screen = node.screen
    except Exception:
        screen = None
    for key_method_name in event.name_aliases:
        if (key_method := get_key_handler(node, key_method_name)) is not None:
            if invoked_method:
                _raise_duplicate_key_handlers_error(
                    key_name, invoked_method.__name__, key_method.__name__
                )
            # If key handlers return False, then they are not considered handled
            # This allows key handlers to do some conditional logic

View on GitHub (pinned to 06dbeef4bb)