Textualize/textual · error · TooManyMatches

Call to query_one resulted in more than one matched node

Error message

Call to query_one resulted in more than one matched node

What it means

query_exactly_one (and its TooManyMatches error) guarantees at most one match: after finding a first matching node it continues scanning siblings, and if any later node also matches, TooManyMatches is raised.

Source

Thrown at src/textual/dom.py:1643

            cache_key = (base_node._nodes._updates, query_selector, expect_type)
            cached_result = base_node._query_one_cache.get(cache_key)
            if cached_result is not None:
                return cached_result
        else:
            cache_key = None

        children = walk_breadth_first(base_node, with_root=False)
        iter_children = iter(children)
        for node in iter_children:
            if not match(selector_set, node):
                continue
            if expect_type is not None and not isinstance(node, expect_type):
                raise WrongType(
                    f"Node matching {query_selector!r} is the wrong type; expected type {expect_type.__name__!r}, found {node}"
                )
            for later_node in iter_children:
                if match(selector_set, later_node):
                    raise TooManyMatches(
                        "Call to query_one resulted in more than one matched node"
                    )
            if cache_key is not None:
                base_node._query_one_cache[cache_key] = node
            return node

        raise NoMatches(f"No nodes match {query_selector!r} on {base_node!r}")

    if TYPE_CHECKING:

        @overload
        def query_ancestor(self, selector: str) -> DOMNode: ...

        @overload
        def query_ancestor(self, selector: type[QueryType]) -> QueryType: ...

        @overload
        def query_ancestor(

View on GitHub (pinned to 06dbeef4bb)

Solutions

  1. Use a unique id for the single target and query by '#id'
  2. Remove/clear previously mounted duplicates before mounting new ones (e.g. remove_children then mount)
  3. If multiple matches are expected, use query() and index/filter the results instead of query_exactly_one

Example fix

# before
await self.mount(Button('Go', classes='primary'))
node = self.query_exactly_one('.primary')  # TooManyMatches after second mount

# after
await self.mount(Button('Go', id='go', classes='primary'))
node = self.query_exactly_one('#go')
Defensive patterns

Strategy: validation

Validate before calling

matches = self.query('.primary')
if len(matches.nodes) > 1:
    # narrow the selector before the strict call
    selector = '#go'
node = self.query_exactly_one(selector)

Type guard

def is_unique(base, selector: str) -> bool:
    return len(base.query(selector).nodes) == 1

Try / catch

from textual.css.query import TooManyMatches, NoMatches
try:
    node = self.query_exactly_one('#go')
except TooManyMatches:
    node = self.query('#go').first()  # or fix duplicates
except NoMatches:
    node = None

Prevention

When it happens

Trigger: Calling query_exactly_one('.item') or query_one('#dup-id'-style broad selectors when two or more nodes in the subtree match — e.g. duplicate classes like two Buttons with class 'primary'.

Common situations: Repeatedly mounting widgets with the same classes in a loop; screens accumulating duplicate widgets because a previous one wasn't removed; class-based styling classes reused for looks being used for queries.

Related errors


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