python-telegram-bot/python-telegram-bot · error · RuntimeError

Can't build key for update without CallbackQuery!

Error message

Can't build key for update without CallbackQuery!

What it means

When per_message=True, ConversationHandler keys conversations by the message the callback query is attached to. If an update being checked has no callback_query at all (e.g. a Message or InlineQuery update), the key cannot be built and RuntimeError is raised in _get_key.

Source

Thrown at src/telegram/ext/_handlers/conversationhandler.py:649

        """Builds the conversation key associated with the update."""
        chat = update.effective_chat
        user = update.effective_user

        key: list[int | str] = []

        if self.per_chat:
            if chat is None:
                raise RuntimeError("Can't build key for update without effective chat!")
            key.append(chat.id)

        if self.per_user:
            if user is None:
                raise RuntimeError("Can't build key for update without effective user!")
            key.append(user.id)

        if self.per_message:
            if update.callback_query is None:
                raise RuntimeError("Can't build key for update without CallbackQuery!")
            if update.callback_query.inline_message_id:
                key.append(update.callback_query.inline_message_id)
            else:
                key.append(update.callback_query.message.message_id)  # type: ignore[union-attr]

        return tuple(key)

    async def _schedule_job_delayed(
        self,
        new_state: asyncio.Task,
        application: "Application[Any, CCT, Any, Any, Any, JobQueue]",
        update: Update,
        context: CCT,
        conversation_key: ConversationKey,
    ) -> None:
        try:
            effective_new_state = await new_state
        except Exception as exc:

View on GitHub (pinned to d3b69d2e9f)

Solutions

  1. Make sure entry_points and all state handlers are CallbackQueryHandler instances when per_message=True
  2. Add a check_update guard: use a TypeHandler(CallbackQuery, ...) wrapper so only callback updates reach the conversation
  3. If the conversation must handle both messages and callbacks, use per_message=False with per_chat/per_user instead

Example fix

# before
ch = ConversationHandler(
    entry_points=[CommandHandler('start', start)],  # message handler + per_message
    states={1: [CallbackQueryHandler(one)]},
    per_message=True,
)

# after
ch = ConversationHandler(
    entry_points=[CallbackQueryHandler(start, pattern='^start$')],
    states={1: [CallbackQueryHandler(one)]},
    per_message=True,
)
Defensive patterns

Strategy: validation

Validate before calling

def is_per_message_safe(update: Update) -> bool:
    return update.callback_query is not None
# only feed callback updates to per_message conversations

Type guard

def is_callback_update(update: Update) -> bool:
    return update.callback_query is not None

Prevention

When it happens

Trigger: ConversationHandler(per_message=True) whose check_update is invoked for a non-callback update (any Message, EditedMessage, InlineQuery, etc.). The per_message branch unconditionally accesses update.callback_query and raises if it is None.

Common situations: Building a per_message conversation but adding entry points or state handlers that match plain messages (common mistake — per_message=True should use CallbackQueryHandler entry points only). Triggered from check_update whenever a text message arrives while the handler is in a state that matches it.

Related errors


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