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
- Wrap the row(s): ReplyKeyboardMarkup([['A','B']])
- Ensure every inner element is a str or KeyboardButton
- 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
- Always nest rows one level deeper than the buttons
- Construct rows with comprehensions: [[KeyboardButton(t) for t in row] for row in labels]]
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
- The parameter `inline_keyboard` should be a sequence of sequ
- t is an (absolute) datetime while reference_timestamp is not
- persistence must be based on telegram.ext.BasePersistence
- Either contact or phone_number and first_name must be passed
- Either contact or phone_number and first_name must be passed
AI-assisted analysis of python-telegram-bot/python-telegram-bot@d3b69d2e9f (2026-08-28).
Data as JSON: /api/errors/6db2a1c233cfe9aa.
Report an issue: GitHub.