Textualize/rich · error · ValueError

level must be 'head', 'row' or 'foot'

Error message

level must be 'head', 'row' or 'foot'

What it means

Box.row (the method that renders one horizontal rule of a box character set) only accepts level values "head", "row", "mid", or "foot"; anything else raises ValueError("level must be 'head', 'row' or 'foot'"). This is essentially an internal API used by Table rendering — the error message even forgets to mention the accepted "mid" level. End users normally never call it directly.

Solutions

  1. Pass one of "head", "row", "mid", or "foot" as the level
  2. If you don't need a semantic row type, use the default (typically "row")
  3. Prefer composing boxes via the predefined box constants (box.SQUARE, box.DOUBLE, ...) rather than calling Box.row yourself

Example fix

// before (Python)
sep = box.SQUARE.row([], widths, level="header")  # ValueError
// after
sep = box.SQUARE.row([], widths, level="head")
Defensive patterns

Strategy: validation

Validate before calling

if level not in ("head", "row", "mid", "foot"):
    raise ValueError(f"invalid box level {level!r}")
line = box.SQUARE.row(characters, widths, level=level)

Type guard

from typing import Literal, TypeGuard

def is_box_level(v: object) -> TypeGuard[str]:
    return v in ("head", "row", "mid", "foot")

Prevention

When it happens

Trigger: Calling box.SQUARE.row(head_row, widths, level=...) with a misspelled or unknown level string; custom Table/box subclasses or plugins that invoke Box.row directly with their own level names; passing None or an empty string.

Common situations: Almost exclusively seen when writing custom table renderers or third-party extensions that reuse Box.row; occasionally triggered by copying example code that hardcodes an invalid level.

Related errors


AI-assisted analysis of Textualize/rich@9d8f9a372c (2026-08-15). Data as JSON: /api/errors/da741d623d7f922e. Report an issue: GitHub.

Appendix: source

Thrown at rich/box.py:150

            cross = self.head_row_cross
            right = self.head_row_right
        elif level == "row":
            left = self.row_left
            horizontal = self.row_horizontal
            cross = self.row_cross
            right = self.row_right
        elif level == "mid":
            left = self.mid_left
            horizontal = " "
            cross = self.mid_vertical
            right = self.mid_right
        elif level == "foot":
            left = self.foot_row_left
            horizontal = self.foot_row_horizontal
            cross = self.foot_row_cross
            right = self.foot_row_right
        else:
            raise ValueError("level must be 'head', 'row' or 'foot'")

        parts: List[str] = []
        append = parts.append
        if edge:
            append(left)
        for last, width in loop_last(widths):
            append(horizontal * width)
            if not last:
                append(cross)
        if edge:
            append(right)
        return "".join(parts)

    def get_bottom(self, widths: Iterable[int]) -> str:
        """Get the bottom of a simple box.

        Args:
            widths (List[int]): Widths of columns.

View on GitHub (pinned to 9d8f9a372c)