chroma-core/chroma · error · TypeError

$sub requires a dict with 'left' and 'right', got {type(sub_

Error message

$sub requires a dict with 'left' and 'right', got {type(sub_data).__name__}

What it means

When deserializing a rank expression, Chroma's $sub operator models binary subtraction and expects a dict with 'left' and 'right' keys, unlike $sum/$mul which take operand lists. Rank.from_dict raises this TypeError when the $sub payload is not a dict (e.g. a two-element list, mirroring $sum's shape, or a scalar).

Source

Thrown at chromadb/execution/expression/operator.py:771

            if not isinstance(ranks_data, (list, tuple)):
                raise TypeError(
                    f"$sum requires a list, got {type(ranks_data).__name__}"
                )
            if len(ranks_data) < 2:
                raise ValueError(
                    f"$sum requires at least 2 ranks, got {len(ranks_data)}"
                )

            ranks = [Rank.from_dict(r) for r in ranks_data]
            result = ranks[0]
            for r in ranks[1:]:
                result = result + r
            return result

        elif op == "$sub":
            sub_data = data["$sub"]
            if not isinstance(sub_data, dict):
                raise TypeError(
                    f"$sub requires a dict with 'left' and 'right', got {type(sub_data).__name__}"
                )
            if "left" not in sub_data or "right" not in sub_data:
                raise ValueError("$sub requires 'left' and 'right' fields")

            left = Rank.from_dict(sub_data["left"])
            right = Rank.from_dict(sub_data["right"])
            return left - right

        elif op == "$mul":
            ranks_data = data["$mul"]
            if not isinstance(ranks_data, (list, tuple)):
                raise TypeError(
                    f"$mul requires a list, got {type(ranks_data).__name__}"
                )
            if len(ranks_data) < 2:
                raise ValueError(
                    f"$mul requires at least 2 ranks, got {len(ranks_data)}"

View on GitHub (pinned to aecdd12c8a)

Solutions

  1. Encode subtraction as {"$sub": {"left": <rank>, "right": <rank>}} with both keys present
  2. If you prefer list-shaped input, convert before serialization: {'$sub': {'left': pair[0], 'right': pair[1]}}
  3. Build the expression in Python as left_rank - right_rank and call .to_dict() to get a guaranteed-valid shape
  4. Guard with isinstance(payload, dict) before calling Rank.from_dict

Example fix

# before
expr = {"$sub": [dense_rank, sparse_rank]}

# after
expr = {"$sub": {"left": dense_rank, "right": sparse_rank}}
Defensive patterns

Strategy: validation

Validate before calling

def valid_sub_expr(expr):
    return (
        isinstance(expr, dict) and set(expr) == {"$sub"}
        and isinstance(expr["$sub"], dict)
    )

if not valid_sub_expr(rank_expr):
    raise ValueError(f"$sub payload must be a dict with left/right, got {rank_expr!r}")

Type guard

def is_sub_expr(d) -> bool:
    return (isinstance(d, dict) and set(d) == {"$sub"}
            and isinstance(d["$sub"], dict))

Try / catch

try:
    rank = Rank.from_dict(rank_expr)
except (TypeError, ValueError) as e:
    raise ValueError(f"invalid rank expression {rank_expr!r}: {e}") from e

Prevention

When it happens

Trigger: Rank.from_dict({'$sub': [left, right]}) or {'$sub': 5}, or the equivalent nested inside a larger expression tree that a client or service deserializes.

Common situations: Developers assume all arithmetic operators share the list shape of $sum/$mul and encode subtraction as a pair array; code that generates operator dicts from a generic 'binary op -> [a, b]' template; JSON written by hand or by another tool that uses the wrong payload shape.

Related errors


AI-assisted analysis of chroma-core/chroma@aecdd12c8a (2026-08-16). Data as JSON: /api/errors/a4be82dd683b4d7b. Report an issue: GitHub.