Repository: python-telegram-bot/python-telegram-bot
Pull request: Fix & improve some examples
- closes #2440 
- some typo fixes
- Handle empty inline queries without raising errors.
- remove unnecessary if condition in passportbot.py
- use `ReplyKeyboardRemove()` when conversation is over.
- use `ForceReply` in an example.

Problem statement:
[BUG] Broken button '4' in inlinekeyboard2.py example
When the inline button '4' in the inlinekeyboard2.py example is clicked, it sends the same button as an option in the keyboard menu. Thus, when the user clicks it again, Telegram raises an error saying new message content and reply markup is same. This can confuse a user.

### Steps to reproduce
1. Run inlinekeyboard2.py, click `/start`.

2. Click button '1'.

3. Click button '4'.

4.  Click button '4' again.
5.  Error raised from Telegram.

Potential fix:
Change which button is offered in the keyboard menu.

Hints from issue discussion:
just change 4 to 2 or so
Personally I also do not particularly enjoy this being considered an error. Editing a message and making it the same should be evaluated including the buttons and not just the message text. Personally, I change it based on the change actioned on the button (my use case has the user choosing who to follow and I edit the message to say "x is now followed" "x is not followed anymore").

Commit message: remove extra whitespace

Patch to review:
diff --git a/examples/conversationbot2.py b/examples/conversationbot2.py
index 0397b649956..98d45aef879 100644
--- a/examples/conversationbot2.py
+++ b/examples/conversationbot2.py
@@ -17,7 +17,7 @@
 import logging
 from typing import Dict
 
-from telegram import ReplyKeyboardMarkup, Update
+from telegram import ReplyKeyboardMarkup, Update, ReplyKeyboardRemove
 from telegram.ext import (
     Updater,
     CommandHandler,
@@ -102,7 +102,8 @@ def done(update: Update, context: CallbackContext) -> int:
         del user_data['choice']
 
     update.message.reply_text(
-        f"I learned these facts about you: {facts_to_str(user_data)} Until next time!"
+        f"I learned these facts about you: {facts_to_str(user_data)}Until next time!",
+        reply_markup=ReplyKeyboardRemove(),
     )
 
     user_data.clear()
diff --git a/examples/deeplinking.py b/examples/deeplinking.py
index 340b0512223..7df2864b340 100644
--- a/examples/deeplinking.py
+++ b/examples/deeplinking.py
@@ -44,7 +44,7 @@
 USING_KEYBOARD = "using-keyboard-here"
 SO_COOL = "so-cool"
 
-# Callback data to pass in 3rd level deeplinking
+# Callback data to pass in 3rd level deep-linking
 KEYBOARD_CALLBACKDATA = "keyboard-callback-data"
 
 
@@ -62,7 +62,7 @@ def deep_linked_level_1(update: Update, context: CallbackContext) -> None:
     url = helpers.create_deep_linked_url(bot.username, SO_COOL)
     text = (
         "Awesome, you just accessed hidden functionality! "
-        " Now let's get back to the private chat."
+        "Now let's get back to the private chat."
     )
     keyboard = InlineKeyboardMarkup.from_button(
         InlineKeyboardButton(text="Continue here!", url=url)
@@ -127,7 +127,7 @@ def main() -> None:
         CommandHandler("start", deep_linked_level_3, Filters.regex(USING_ENTITIES), pass_args=True)
     )
 
-    # Possible with inline keyboard buttons aswell
+    # Possible with inline keyboard buttons as well
     dispatcher.add_handler(
         CommandHandler("start", deep_linked_level_4, Filters.regex(USING_KEYBOARD))
     )
diff --git a/examples/inlinekeyboard2.py b/examples/inlinekeyboard2.py
index 800f04c61c5..fa16c40731b 100644
--- a/examples/inlinekeyboard2.py
+++ b/examples/inlinekeyboard2.py
@@ -139,7 +139,7 @@ def four(update: Update, _: CallbackContext) -> int:
     keyboard = [
         [
             InlineKeyboardButton("2", callback_data=str(TWO)),
-            InlineKeyboardButton("4", callback_data=str(FOUR)),
+            InlineKeyboardButton("3", callback_data=str(THREE)),
         ]
     ]
     reply_markup = InlineKeyboardMarkup(keyboard)
@@ -188,8 +188,7 @@ def main() -> None:
         fallbacks=[CommandHandler('start', start)],
     )
 
-    # Add ConversationHandler to dispatcher that will be used for handling
-    # updates
+    # Add ConversationHandler to dispatcher that will be used for handling updates
     dispatcher.add_handler(conv_handler)
 
     # Start the Bot
diff --git a/examples/nestedconversationbot.py b/examples/nestedconversationbot.py
index 6c7b880746d..0ce9eee01bc 100644
--- a/examples/nestedconversationbot.py
+++ b/examples/nestedconversationbot.py
@@ -74,9 +74,10 @@ def _name_switcher(level: str) -> Tuple[str, str]:
 def start(update: Update, context: CallbackContext) -> str:
     """Select an action: Adding parent/child or show data."""
     text = (
-        'You may add a familiy member, yourself show the gathered data or end the '
-        'conversation. To abort, simply type /stop.'
+        "You may choose to add a family member, yourself, show the gathered data, or end the "
+        "conversation. To abort, simply type /stop."
     )
+
     buttons = [
         [
             InlineKeyboardButton(text='Add family member', callback_data=str(ADDING_MEMBER)),
@@ -89,13 +90,13 @@ def start(update: Update, context: CallbackContext) -> str:
     ]
     keyboard = InlineKeyboardMarkup(buttons)
 
-    # If we're starting over we don't need do send a new message
+    # If we're starting over we don't need to send a new message
     if context.user_data.get(START_OVER):
         update.callback_query.answer()
         update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
     else:
         update.message.reply_text(
-            'Hi, I\'m FamiliyBot and here to help you gather information' 'about your family.'
+            "Hi, I'm Family Bot and I'm here to help you gather information about your family."
         )
         update.message.reply_text(text=text, reply_markup=keyboard)
 
@@ -104,7 +105,7 @@ def start(update: Update, context: CallbackContext) -> str:
 
 
 def adding_self(update: Update, context: CallbackContext) -> str:
-    """Add information about youself."""
+    """Add information about yourself."""
     context.user_data[CURRENT_LEVEL] = SELF
     text = 'Okay, please tell me about yourself.'
     button = InlineKeyboardButton(text='Add info', callback_data=str(MALE))
@@ -137,9 +138,9 @@ def prettyprint(user_data: Dict[str, Any], level: str) -> str:
         return text
 
     user_data = context.user_data
-    text = 'Yourself:' + prettyprint(user_data, SELF)
-    text += '\n\nParents:' + prettyprint(user_data, PARENTS)
-    text += '\n\nChildren:' + prettyprint(user_data, CHILDREN)
+    text = f"Yourself:{prettyprint(user_data, SELF)}"
+    text += f"\n\nParents:{prettyprint(user_data, PARENTS)}"
+    text += f"\n\nChildren:{prettyprint(user_data, CHILDREN)}"
 
     buttons = [[InlineKeyboardButton(text='Back', callback_data=str(END))]]
     keyboard = InlineKeyboardMarkup(buttons)
@@ -201,8 +202,8 @@ def select_gender(update: Update, context: CallbackContext) -> str:
 
     buttons = [
         [
-            InlineKeyboardButton(text='Add ' + male, callback_data=str(MALE)),
-            InlineKeyboardButton(text='Add ' + female, callback_data=str(FEMALE)),
+            InlineKeyboardButton(text=f'Add {male}', callback_data=str(MALE)),
+            InlineKeyboardButton(text=f'Add {female}', callback_data=str(FEMALE)),
         ],
         [
             InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
@@ -326,7 +327,7 @@ def main() -> None:
         map_to_parent={
             # Return to second level menu
             END: SELECTING_LEVEL,
-            # End conversation alltogether
+            # End conversation altogether
             STOPPING: STOPPING,
         },
     )
@@ -350,13 +351,13 @@ def main() -> None:
             SHOWING: SHOWING,
             # Return to top level menu
             END: SELECTING_ACTION,
-            # End conversation alltogether
+            # End conversation altogether
             STOPPING: END,
         },
     )
 
     # Set up top level ConversationHandler (selecting action)
-    # Because the states of the third level conversation map to the ones of the econd level
+    # Because the states of the third level conversation map to the ones of the second level
     # conversation, we need to make sure the top level conversation can also handle them
     selection_handlers = [
         add_member_conv,
diff --git a/examples/passportbot.py b/examples/passportbot.py
index 195e1cdc47c..4c57170a339 100644
--- a/examples/passportbot.py
+++ b/examples/passportbot.py
@@ -24,75 +24,74 @@
 
 
 def msg(update: Update, _: CallbackContext) -> None:
-    # If we received any passport data
+    # Retrieve passport data
     passport_data = update.message.passport_data
-    if passport_data:
-        # If our nonce doesn't match what we think, this Update did not originate from us
-        # Ideally you would randomize the nonce on the server
-        if passport_data.decrypted_credentials.nonce != 'thisisatest':
-            return
-
-        # Print the decrypted credential data
-        # For all elements
-        # Print their decrypted data
-        # Files will be downloaded to current directory
-        for data in passport_data.decrypted_data:  # This is where the data gets decrypted
-            if data.type == 'phone_number':
-                print('Phone: ', data.phone_number)
-            elif data.type == 'email':
-                print('Email: ', data.email)
-            if data.type in (
-                'personal_details',
-                'passport',
-                'driver_license',
-                'identity_card',
-                'internal_passport',
-                'address',
-            ):
-                print(data.type, data.data)
-            if data.type in (
-                'utility_bill',
-                'bank_statement',
-                'rental_agreement',
-                'passport_registration',
-                'temporary_registration',
-            ):
-                print(data.type, len(data.files), 'files')
-                for file in data.files:
-                    actual_file = file.get_file()
-                    print(actual_file)
-                    actual_file.download()
-            if data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport'):
-                if data.front_side:
-                    front_file = data.front_side.get_file()
-                    print(data.type, front_file)
-                    front_file.download()
-            if data.type in ('driver_license' and 'identity_card'):
-                if data.reverse_side:
-                    reverse_file = data.reverse_side.get_file()
-                    print(data.type, reverse_file)
-                    reverse_file.download()
-            if data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport'):
-                if data.selfie:
-                    selfie_file = data.selfie.get_file()
-                    print(data.type, selfie_file)
-                    selfie_file.download()
-            if data.type in (
-                'passport',
-                'driver_license',
-                'identity_card',
-                'internal_passport',
-                'utility_bill',
-                'bank_statement',
-                'rental_agreement',
-                'passport_registration',
-                'temporary_registration',
-            ):
-                print(data.type, len(data.translation), 'translation')
-                for file in data.translation:
-                    actual_file = file.get_file()
-                    print(actual_file)
-                    actual_file.download()
+    # If our nonce doesn't match what we think, this Update did not originate from us
+    # Ideally you would randomize the nonce on the server
+    if passport_data.decrypted_credentials.nonce != 'thisisatest':
+        return
+
+    # Print the decrypted credential data
+    # For all elements
+    # Print their decrypted data
+    # Files will be downloaded to current directory
+    for data in passport_data.decrypted_data:  # This is where the data gets decrypted
+        if data.type == 'phone_number':
+            print('Phone: ', data.phone_number)
+        elif data.type == 'email':
+            print('Email: ', data.email)
+        if data.type in (
+            'personal_details',
+            'passport',
+            'driver_license',
+            'identity_card',
+            'internal_passport',
+            'address',
+        ):
+            print(data.type, data.data)
+        if data.type in (
+            'utility_bill',
+            'bank_statement',
+            'rental_agreement',
+            'passport_registration',
+            'temporary_registration',
+        ):
+            print(data.type, len(data.files), 'files')
+            for file in data.files:
+                actual_file = file.get_file()
+                print(actual_file)
+                actual_file.download()
+        if data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport'):
+            if data.front_side:
+                front_file = data.front_side.get_file()
+                print(data.type, front_file)
+                front_file.download()
+        if data.type in ('driver_license' and 'identity_card'):
+            if data.reverse_side:
+                reverse_file = data.reverse_side.get_file()
+                print(data.type, reverse_file)
+                reverse_file.download()
+        if data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport'):
+            if data.selfie:
+                selfie_file = data.selfie.get_file()
+                print(data.type, selfie_file)
+                selfie_file.download()
+        if data.type in (
+            'passport',
+            'driver_license',
+            'identity_card',
+            'internal_passport',
+            'utility_bill',
+            'bank_statement',
+            'rental_agreement',
+            'passport_registration',
+            'temporary_registration',
+        ):
+            print(data.type, len(data.translation), 'translation')
+            for file in data.translation:
+                actual_file = file.get_file()
+                print(actual_file)
+                actual_file.download()
 
 
 def main() -> None:
diff --git a/examples/paymentbot.py b/examples/paymentbot.py
index 34f93df93e4..6b38f2cfd9c 100644
--- a/examples/paymentbot.py
+++ b/examples/paymentbot.py
@@ -28,8 +28,11 @@
 
 
 def start_callback(update: Update, _: CallbackContext) -> None:
-    msg = "Use /shipping to get an invoice for shipping-payment, "
-    msg += "or /noshipping for an invoice without shipping."
+    msg = (
+        "Use /shipping to get an invoice for shipping-payment, or /noshipping for an "
+        "invoice without shipping."
+    )
+
     update.message.reply_text(msg)
 
 
diff --git a/examples/persistentconversationbot.py b/examples/persistentconversationbot.py
index 686f737f0b8..80ded339211 100644
--- a/examples/persistentconversationbot.py
+++ b/examples/persistentconversationbot.py
@@ -17,7 +17,7 @@
 import logging
 from typing import Dict
 
-from telegram import ReplyKeyboardMarkup, Update
+from telegram import ReplyKeyboardMarkup, Update, ReplyKeyboardRemove, ForceReply
 from telegram.ext import (
     Updater,
     CommandHandler,
@@ -77,18 +77,19 @@ def regular_choice(update: Update, context: CallbackContext) -> int:
     context.user_data['choice'] = text
     if context.user_data.get(text):
         reply_text = (
-            f'Your {text}, I already know the following about that: {context.user_data[text]}'
+            f'Your {text}? I already know the following about that: {context.user_data[text]}'
         )
     else:
         reply_text = f'Your {text}? Yes, I would love to hear about that!'
-    update.message.reply_text(reply_text)
+    update.message.reply_text(reply_text, reply_markup=ForceReply(selective=True))
 
     return TYPING_REPLY
 
 
 def custom_choice(update: Update, _: CallbackContext) -> int:
     update.message.reply_text(
-        'Alright, please send me the category first, ' 'for example "Most impressive skill"'
+        'Alright, please send me the category first, for example "Most impressive skill"',
+        reply_markup=ForceReply(selective=True),  # force a reply from the user
     )
 
     return TYPING_CHOICE
@@ -122,7 +123,8 @@ def done(update: Update, context: CallbackContext) -> int:
         del context.user_data['choice']
 
     update.message.reply_text(
-        "I learned these facts about you:" f"{facts_to_str(context.user_data)} Until next time!"
+        "I learned these facts about you:" f"{facts_to_str(context.user_data)}Until next time!",
+        reply_markup=ReplyKeyboardRemove(),
     )
     return ConversationHandler.END
 
diff --git a/examples/pollbot.py b/examples/pollbot.py
index 8e4a6d13d06..e2ea0f112c5 100644
--- a/examples/pollbot.py
+++ b/examples/pollbot.py
@@ -4,7 +4,7 @@
 
 """
 Basic example for a bot that works with polls. Only 3 people are allowed to interact with each
-poll/quiz the bot generates. The preview command generates a closed poll/quiz, excatly like the
+poll/quiz the bot generates. The preview command generates a closed poll/quiz, exactly like the
 one the user sends the bot
 """
 import logging


Code review comments:
diff_hunk: @@ -77,18 +77,19 @@ def regular_choice(update: Update, context: CallbackContext) -> int:
     context.user_data['choice'] = text
     if context.user_data.get(text):
         reply_text = (
-            f'Your {text}, I already know the following about that: {context.user_data[text]}'
+            f'Your {text}? I already know the following about that: {context.user_data[text]}'
         )
     else:
         reply_text = f'Your {text}? Yes, I would love to hear about that!'
-    update.message.reply_text(reply_text)
+    update.message.reply_text(reply_text, reply_markup=ForceReply(selective=True))
path: examples/persistentconversationbot.py
text: @user1:
Mh, if people ask for examples on how to use `ForceReply` I feel like it's not a good idea to have them run a conversationbot … Could you mind moving this to `echobot.py`? You can have the `start` message mention the user, which should work with `selective=True`

@author:
Alright. I included this here cause it felt the like the most natural place to put it.