python-telegram-bot/python-telegram-bot · error · ValueError

The parameter `keyboard` should be a sequence of sequences o

Error message

The parameter `keyboard` should be a sequence of sequences of strings or KeyboardButtons

What it means

ReplyKeyboardMarkup rejected its keyboard argument because it is not a sequence of sequences of strings/KeyboardButtons. The constructor validates the shape eagerly so malformed keyboards fail at construction rather than at send time.

Source

Thrown at src/telegram/_replykeyboardmarkup.py:146

        "one_time_keyboard",
        "resize_keyboard",
        "selective",
    )

    def __init__(
        self,
        keyboard: Sequence[Sequence[str | KeyboardButton]],
        resize_keyboard: bool | None = None,
        one_time_keyboard: bool | None = None,
        selective: bool | None = None,
        input_field_placeholder: str | None = None,
        is_persistent: bool | None = None,
        *,
        api_kwargs: JSONDict | None = None,
    ):
        super().__init__(api_kwargs=api_kwargs)
        if not check_keyboard_type(keyboard):
            raise ValueError(
                "The parameter `keyboard` should be a sequence of sequences of "
                "strings or KeyboardButtons"
            )

        # Required
        self.keyboard: tuple[tuple[KeyboardButton, ...], ...] = tuple(
            tuple(KeyboardButton(button) if isinstance(button, str) else button for button in row)
            for row in keyboard
        )

        # Optionals
        self.resize_keyboard: bool | None = resize_keyboard
        self.one_time_keyboard: bool | None = one_time_keyboard
        self.selective: bool | None = selective
        self.input_field_placeholder: str | None = input_field_placeholder
        self.is_persistent: bool | None = is_persistent

        self._id_attrs = (self.keyboard,)

View on GitHub (pinned to d3b69d2e9f)

Solutions

  1. Wrap the row(s): ReplyKeyboardMarkup([['A','B']])
  2. Ensure every inner element is a str or KeyboardButton
  3. When building dynamically, use a list comprehension producing rows: [[KeyboardButton(b) for b in row] for row in buttons]

Example fix

# before
kb = ReplyKeyboardMarkup(['Yes', 'No'])

# after
kb = ReplyKeyboardMarkup([['Yes', 'No']])
Defensive patterns

Strategy: type-guard

Validate before calling

null

Type guard

from telegram import KeyboardButton

def is_valid_keyboard(kb) -> bool:
    try:
        return all(
            isinstance(row, (list, tuple))
            and all(isinstance(b, (str, KeyboardButton)) for b in row)
            for row in kb
        )
    except TypeError:
        return False

Try / catch

null

Prevention

When it happens

Trigger: Passing a flat list like ['A','B'] instead of [['A','B']], passing tuples of tuples with non-button items, a generator, or None to ReplyKeyboardMarkup(keyboard=...).

Common situations: Dynamically building keyboards from user data; refactoring from InlineKeyboardMarkup (which uses rows of dict/InlineKeyboardButton) to ReplyKeyboardMarkup.

Related errors


AI-assisted analysis of python-telegram-bot/python-telegram-bot@d3b69d2e9f (2026-08-28). Data as JSON: /api/errors/6db2a1c233cfe9aa. Report an issue: GitHub.