kovidgoyal/kitty · error · ParseException

Failed to parse {query!r}, too much recursion required

Error message

Failed to parse {query!r}, too much recursion required

What it means

Raised when the recursive-descent parser blows Python's recursion limit while parsing a search query, i.e. the expression is too deeply nested or too long. The underlying RecursionError from parse() is caught in build_tree and re-raised as a ParseException with this message.

Source

Thrown at kitty/search_query_parser.py:290

                tt = self.token(advance=True)
                assert tt is not None
                return TokenNode(loc, tt)
            return TokenNode(loc.lower(), ':'.join(words))

        if self.allow_no_location:
            return TokenNode('all', ':'.join(words))
        raise NoLocation(tt)


@lru_cache(maxsize=64)
def build_tree(query: str, locations: str | tuple[str, ...], allow_no_location: bool = False) -> SearchTreeNode:
    if isinstance(locations, str):
        locations = tuple(locations.split())
    p = Parser(allow_no_location)
    try:
        return p.parse(query, locations)
    except RuntimeError as e:
        raise ParseException(f'Failed to parse {query!r}, too much recursion required') from e


def search(
    query: str,
    locations: str | tuple[str, ...],
    universal_set: set[T],
    get_matches: GetMatches[T],
    allow_no_location: bool = False,
) -> set[T]:
    return build_tree(query, locations, allow_no_location).search(universal_set, get_matches)

View on GitHub (pinned to 6d5d0c4406)

Solutions

  1. Flatten the query: use a flat chain of 'and'/'or' instead of nested parentheses
  2. Limit or batch the terms you feed into one query
  3. Catch ParseException at the call site and reject/split the query
  4. If legitimately deep nesting is required, raise sys.setrecursionlimit cautiously — better to restructure the query

Example fix

# before
q = '(' * 5000 + 'foo' + ')' * 5000
build_tree(q, locations)
# after
q = ' and '.join(terms)  # flat conjunction
build_tree(q, locations)
Defensive patterns

Strategy: try-catch

Validate before calling

def depth_ok(q: str, limit: int = 200) -> bool:
    depth = mx = 0
    for ch in q:
        if ch == '(':
            depth += 1
            mx = max(mx, depth)
        elif ch == ')':
            depth -= 1
    return mx <= limit and len(q) < 100_000

Try / catch

from kitty.search_query_parser import ParseException
try:
    node = build_tree(query, locations)
except ParseException as e:
    if 'too much recursion' in str(e):
        query = flatten_query(query)  # or split into batches
    else:
        raise

Prevention

When it happens

Trigger: Queries with very deep parenthesis nesting (hundreds of levels), extremely long chains of 'and'/'or' terms, or machine-generated queries of pathological length passed to kitty.search.query_parser.search()/build_tree().

Common situations: Programmatically generated queries from large term lists; adversarial/fuzzed input fed to the parser; nesting from auto-wrapping logic that adds parens per iteration.

Understand the failure class

Related errors


AI-assisted analysis of kovidgoyal/kitty@6d5d0c4406 (2026-08-27). Data as JSON: /api/errors/399105e7f9bfbced. Report an issue: GitHub.