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
- Use a unique id for the single target and query by '#id'
- Remove/clear previously mounted duplicates before mounting new ones (e.g. remove_children then mount)
- 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
- Give singleton widgets unique ids
- Remove old widgets before re-mounting
- Reserve styling classes for CSS, ids for queries
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
- No nodes match {self!r} on {self.node!r}
- Call to only_one resulted in more than one matched node
- No nodes match {self!r} on dom{self.node!r}
- Node matching {query_selector!r} is the wrong type; expected
- No nodes match {query_selector!r} on {base_node!r}
AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27).
Data as JSON: /api/errors/5f7f44fe29b0bd35.
Report an issue: GitHub.