Repository: python-telegram-bot/python-telegram-bot

Issue:
Improvements of types.SLT
Had this thought while reviewing #2890: Currently in a number of places we accept either a single item (say a string) or a tuple/list of containing items of the same type. Using concrete types here originally was motivated because we had documented as `Iterable` in a few places - iterables allow generators, which might be inifininte and will also be consumed.

Anyway, time has passed and I've learned a bit more. In most of those cases, we just want to make sure that  we get a finite number of items - this is covered by [`Collection`](https://docs.python.org/3/library/collections.abc.html?highlight=mapping#collections-abstract-base-classes). If in addition, we need item access or loop over the items in reversed order, we can use `Sequence`.

So I'd suggest to replace the current `types.SLT` with two new types for

* `Union[T, Collection[T]]` and
* `Union[T, Sequence[T]]`

and adjusting the documentation accordingly. I imagine the transition not be to be hard: We replace `SLT` with `Union[T, Collection[T]]` everywhere. If we need it to be `Union[T, Sequence[T]]` somewhere, then mypy will hopefully complain :)

This change would be non-breaking, so not necessarily urgent for v14.


Discussion:
sometimes set is needed, is it ok to make a SLTS for it?
Do you have an example where we'd explicitly need `Union[T, List[T], Tuple[T, …], Set[T]]`?

Resolving patch:
diff --git a/telegram/_utils/types.py b/telegram/_utils/types.py
--- a/telegram/_utils/types.py
+++ b/telegram/_utils/types.py
@@ -24,7 +24,7 @@
     the changelog.
 """
 from pathlib import Path
-from typing import IO, TYPE_CHECKING, Any, Dict, List, Optional, Tuple, TypeVar, Union
+from typing import IO, TYPE_CHECKING, Any, Dict, Optional, Tuple, TypeVar, Union, Collection
 
 if TYPE_CHECKING:
     from telegram import InputFile
@@ -54,8 +54,8 @@
 as ``Union[DefaultValue, type]``."""
 
 RT = TypeVar("RT")
-SLT = Union[RT, List[RT], Tuple[RT, ...]]
-"""Single instance or list/tuple of instances."""
+SCT = Union[RT, Collection[RT]]
+"""Single instance or collection of instances."""
 
 ReplyMarkup = Union[
     "InlineKeyboardMarkup", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", "ForceReply"
diff --git a/telegram/ext/_commandhandler.py b/telegram/ext/_commandhandler.py
--- a/telegram/ext/_commandhandler.py
+++ b/telegram/ext/_commandhandler.py
@@ -22,7 +22,7 @@
 
 from telegram import MessageEntity, Update
 from telegram._utils.defaultvalue import DEFAULT_TRUE
-from telegram._utils.types import SLT, DVInput
+from telegram._utils.types import SCT, DVInput
 from telegram.ext import filters as filters_module
 from telegram.ext._handler import BaseHandler
 from telegram.ext._utils.types import CCT, HandlerCallback
@@ -53,7 +53,7 @@ class CommandHandler(BaseHandler[Update, CCT]):
         attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
 
     Args:
-        command (:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]):
+        command (:obj:`str` | Collection[:obj:`str`]):
             The command or list of commands this handler should listen for.
             Limitations are the same as described `here <https://core.telegram.org/bots#commands>`_
         callback (:term:`coroutine function`): The callback function for this handler. Will be
@@ -76,8 +76,7 @@ async def callback(update: Update, context: CallbackContext)
         :exc:`ValueError`: When the command is too long or has illegal chars.
 
     Attributes:
-        command (:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]):
-            The command or list of commands this handler should listen for.
+        command (List[:obj:`str`]): The list of commands this handler should listen for.
             Limitations are the same as described `here <https://core.telegram.org/bots#commands>`_
         callback (:term:`coroutine function`): The callback function for this handler.
         filters (:class:`telegram.ext.filters.BaseFilter`): Optional. Only allow updates with these
@@ -91,7 +90,7 @@ async def callback(update: Update, context: CallbackContext)
 
     def __init__(
         self,
-        command: SLT[str],
+        command: SCT[str],
         callback: HandlerCallback[Update, CCT, RT],
         filters: filters_module.BaseFilter = None,
         block: DVInput[bool] = DEFAULT_TRUE,
@@ -207,9 +206,9 @@ class PrefixHandler(CommandHandler):
         attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
 
     Args:
-        prefix (:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]):
+        prefix (:obj:`str` | Collection[:obj:`str`]):
             The prefix(es) that will precede :attr:`command`.
-        command (:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]):
+        command (:obj:`str` | Collection[:obj:`str`]):
             The command or list of commands this handler should listen for.
         callback (:term:`coroutine function`): The callback function for this handler. Will be
             called when :meth:`check_update` has determined that an update should be processed by
@@ -242,8 +241,8 @@ async def callback(update: Update, context: CallbackContext)
 
     def __init__(
         self,
-        prefix: SLT[str],
-        command: SLT[str],
+        prefix: SCT[str],
+        command: SCT[str],
         callback: HandlerCallback[Update, CCT, RT],
         filters: filters_module.BaseFilter = None,
         block: DVInput[bool] = DEFAULT_TRUE,
diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py
--- a/telegram/ext/filters.py
+++ b/telegram/ext/filters.py
@@ -107,7 +107,7 @@
 from telegram import Chat as TGChat
 from telegram import Message, MessageEntity, Update
 from telegram import User as TGUser
-from telegram._utils.types import SLT
+from telegram._utils.types import SCT
 from telegram.constants import DiceEmoji as DiceEmojiEnum
 
 DataDict = Dict[str, list]
@@ -588,8 +588,8 @@ class _ChatUserBaseFilter(MessageFilter, ABC):
 
     def __init__(
         self,
-        chat_id: SLT[int] = None,
-        username: SLT[str] = None,
+        chat_id: SCT[int] = None,
+        username: SCT[str] = None,
         allow_empty: bool = False,
     ):
         super().__init__()
@@ -608,7 +608,7 @@ def get_chat_or_user(self, message: Message) -> Union[TGChat, TGUser, None]:
         ...
 
     @staticmethod
-    def _parse_chat_id(chat_id: Optional[SLT[int]]) -> Set[int]:
+    def _parse_chat_id(chat_id: Optional[SCT[int]]) -> Set[int]:
         if chat_id is None:
             return set()
         if isinstance(chat_id, int):
@@ -616,14 +616,14 @@ def _parse_chat_id(chat_id: Optional[SLT[int]]) -> Set[int]:
         return set(chat_id)
 
     @staticmethod
-    def _parse_username(username: Optional[SLT[str]]) -> Set[str]:
+    def _parse_username(username: Optional[SCT[str]]) -> Set[str]:
         if username is None:
             return set()
         if isinstance(username, str):
             return {username[1:] if username.startswith("@") else username}
         return {chat[1:] if chat.startswith("@") else chat for chat in username}
 
-    def _set_chat_ids(self, chat_id: Optional[SLT[int]]) -> None:
+    def _set_chat_ids(self, chat_id: Optional[SCT[int]]) -> None:
         if chat_id and self._usernames:
             raise RuntimeError(
                 f"Can't set {self._chat_id_name} in conjunction with (already set) "
@@ -631,7 +631,7 @@ def _set_chat_ids(self, chat_id: Optional[SLT[int]]) -> None:
             )
         self._chat_ids = self._parse_chat_id(chat_id)
 
-    def _set_usernames(self, username: Optional[SLT[str]]) -> None:
+    def _set_usernames(self, username: Optional[SCT[str]]) -> None:
         if username and self._chat_ids:
             raise RuntimeError(
                 f"Can't set {self._username_name} in conjunction with (already set) "
@@ -644,7 +644,7 @@ def chat_ids(self) -> FrozenSet[int]:
         return frozenset(self._chat_ids)
 
     @chat_ids.setter
-    def chat_ids(self, chat_id: SLT[int]) -> None:
+    def chat_ids(self, chat_id: SCT[int]) -> None:
         self._set_chat_ids(chat_id)
 
     @property
@@ -664,15 +664,15 @@ def usernames(self) -> FrozenSet[str]:
         return frozenset(self._usernames)
 
     @usernames.setter
-    def usernames(self, username: SLT[str]) -> None:
+    def usernames(self, username: SCT[str]) -> None:
         self._set_usernames(username)
 
-    def add_usernames(self, username: SLT[str]) -> None:
+    def add_usernames(self, username: SCT[str]) -> None:
         """
         Add one or more chats to the allowed usernames.
 
         Args:
-            username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]): Which username(s) to
+            username(:obj:`str` | Collection[:obj:`str`]): Which username(s) to
                 allow through. Leading ``'@'`` s in usernames will be discarded.
         """
         if self._chat_ids:
@@ -684,7 +684,7 @@ def add_usernames(self, username: SLT[str]) -> None:
         parsed_username = self._parse_username(username)
         self._usernames |= parsed_username
 
-    def _add_chat_ids(self, chat_id: SLT[int]) -> None:
+    def _add_chat_ids(self, chat_id: SCT[int]) -> None:
         if self._usernames:
             raise RuntimeError(
                 f"Can't set {self._chat_id_name} in conjunction with (already set) "
@@ -695,12 +695,12 @@ def _add_chat_ids(self, chat_id: SLT[int]) -> None:
 
         self._chat_ids |= parsed_chat_id
 
-    def remove_usernames(self, username: SLT[str]) -> None:
+    def remove_usernames(self, username: SCT[str]) -> None:
         """
         Remove one or more chats from allowed usernames.
 
         Args:
-            username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]): Which username(s) to
+            username(:obj:`str` | Collection[:obj:`str`]): Which username(s) to
                 disallow through. Leading ``'@'`` s in usernames will be discarded.
         """
         if self._chat_ids:
@@ -712,7 +712,7 @@ def remove_usernames(self, username: SLT[str]) -> None:
         parsed_username = self._parse_username(username)
         self._usernames -= parsed_username
 
-    def _remove_chat_ids(self, chat_id: SLT[int]) -> None:
+    def _remove_chat_ids(self, chat_id: SCT[int]) -> None:
         if self._usernames:
             raise RuntimeError(
                 f"Can't set {self._chat_id_name} in conjunction with (already set) "
@@ -757,9 +757,9 @@ class Chat(_ChatUserBaseFilter):
         replace the current set of allowed chats.
 
     Args:
-        chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional):
+        chat_id(:obj:`int` | Collection[:obj:`int`], optional):
             Which chat ID(s) to allow through.
-        username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional):
+        username(:obj:`str` | Collection[:obj:`str`], optional):
             Which username(s) to allow through.
             Leading ``'@'`` s in usernames will be discarded.
         allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat
@@ -779,22 +779,22 @@ class Chat(_ChatUserBaseFilter):
     def get_chat_or_user(self, message: Message) -> Optional[TGChat]:
         return message.chat
 
-    def add_chat_ids(self, chat_id: SLT[int]) -> None:
+    def add_chat_ids(self, chat_id: SCT[int]) -> None:
         """
         Add one or more chats to the allowed chat ids.
 
         Args:
-            chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which chat ID(s) to allow
+            chat_id(:obj:`int` | Collection[:obj:`int`]): Which chat ID(s) to allow
                 through.
         """
         return super()._add_chat_ids(chat_id)
 
-    def remove_chat_ids(self, chat_id: SLT[int]) -> None:
+    def remove_chat_ids(self, chat_id: SCT[int]) -> None:
         """
         Remove one or more chats from allowed chat ids.
 
         Args:
-            chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which chat ID(s) to
+            chat_id(:obj:`int` | Collection[:obj:`int`]): Which chat ID(s) to
                 disallow through.
         """
         return super()._remove_chat_ids(chat_id)
@@ -930,7 +930,7 @@ def filter(self, message: Message) -> bool:
 class _Dice(MessageFilter):
     __slots__ = ("emoji", "values")
 
-    def __init__(self, values: SLT[int] = None, emoji: DiceEmojiEnum = None):
+    def __init__(self, values: SCT[int] = None, emoji: DiceEmojiEnum = None):
         super().__init__()
         self.emoji = emoji
         self.values = [values] if isinstance(values, int) else values
@@ -983,7 +983,7 @@ class Dice(_Dice):
         ``filters.TEXT | filters.Dice.ALL``.
 
     Args:
-        values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional):
+        values (:obj:`int` | Collection[:obj:`int`], optional):
             Which values to allow. If not specified, will allow the specified dice message.
     """
 
@@ -996,12 +996,12 @@ class Basketball(_Dice):
         """Dice messages with the emoji 🏀. Supports passing a list of integers.
 
         Args:
-            values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow.
+            values (:obj:`int` | Collection[:obj:`int`]): Which values to allow.
         """
 
         __slots__ = ()
 
-        def __init__(self, values: SLT[int]):
+        def __init__(self, values: SCT[int]):
             super().__init__(values, emoji=DiceEmojiEnum.BASKETBALL)
 
     BASKETBALL = _Dice(emoji=DiceEmojiEnum.BASKETBALL)
@@ -1011,12 +1011,12 @@ class Bowling(_Dice):
         """Dice messages with the emoji 🎳. Supports passing a list of integers.
 
         Args:
-            values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow.
+            values (:obj:`int` | Collection[:obj:`int`]): Which values to allow.
         """
 
         __slots__ = ()
 
-        def __init__(self, values: SLT[int]):
+        def __init__(self, values: SCT[int]):
             super().__init__(values, emoji=DiceEmojiEnum.BOWLING)
 
     BOWLING = _Dice(emoji=DiceEmojiEnum.BOWLING)
@@ -1026,12 +1026,12 @@ class Darts(_Dice):
         """Dice messages with the emoji 🎯. Supports passing a list of integers.
 
         Args:
-            values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow.
+            values (:obj:`int` | Collection[:obj:`int`]): Which values to allow.
         """
 
         __slots__ = ()
 
-        def __init__(self, values: SLT[int]):
+        def __init__(self, values: SCT[int]):
             super().__init__(values, emoji=DiceEmojiEnum.DARTS)
 
     DARTS = _Dice(emoji=DiceEmojiEnum.DARTS)
@@ -1041,12 +1041,12 @@ class Dice(_Dice):
         """Dice messages with the emoji 🎲. Supports passing a list of integers.
 
         Args:
-            values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow.
+            values (:obj:`int` | Collection[:obj:`int`]): Which values to allow.
         """
 
         __slots__ = ()
 
-        def __init__(self, values: SLT[int]):
+        def __init__(self, values: SCT[int]):
             super().__init__(values, emoji=DiceEmojiEnum.DICE)
 
     DICE = _Dice(emoji=DiceEmojiEnum.DICE)  # skipcq: PTC-W0052
@@ -1056,12 +1056,12 @@ class Football(_Dice):
         """Dice messages with the emoji ⚽. Supports passing a list of integers.
 
         Args:
-            values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow.
+            values (:obj:`int` | Collection[:obj:`int`]): Which values to allow.
         """
 
         __slots__ = ()
 
-        def __init__(self, values: SLT[int]):
+        def __init__(self, values: SCT[int]):
             super().__init__(values, emoji=DiceEmojiEnum.FOOTBALL)
 
     FOOTBALL = _Dice(emoji=DiceEmojiEnum.FOOTBALL)
@@ -1071,12 +1071,12 @@ class SlotMachine(_Dice):
         """Dice messages with the emoji 🎰. Supports passing a list of integers.
 
         Args:
-            values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow.
+            values (:obj:`int` | Collection[:obj:`int`]): Which values to allow.
         """
 
         __slots__ = ()
 
-        def __init__(self, values: SLT[int]):
+        def __init__(self, values: SCT[int]):
             super().__init__(values, emoji=DiceEmojiEnum.SLOT_MACHINE)
 
     SLOT_MACHINE = _Dice(emoji=DiceEmojiEnum.SLOT_MACHINE)
@@ -1324,9 +1324,9 @@ class ForwardedFrom(_ChatUserBaseFilter):
         the current set of allowed chats.
 
     Args:
-        chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional):
+        chat_id(:obj:`int` | Collection[:obj:`int`], optional):
             Which chat/user ID(s) to allow through.
-        username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional):
+        username(:obj:`str` | Collection[:obj:`str`], optional):
             Which username(s) to allow through. Leading ``'@'`` s in usernames will be
             discarded.
         allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat
@@ -1346,22 +1346,22 @@ class ForwardedFrom(_ChatUserBaseFilter):
     def get_chat_or_user(self, message: Message) -> Union[TGUser, TGChat, None]:
         return message.forward_from or message.forward_from_chat
 
-    def add_chat_ids(self, chat_id: SLT[int]) -> None:
+    def add_chat_ids(self, chat_id: SCT[int]) -> None:
         """
         Add one or more chats to the allowed chat ids.
 
         Args:
-            chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which chat/user ID(s) to
+            chat_id(:obj:`int` | Collection[:obj:`int`]): Which chat/user ID(s) to
                 allow through.
         """
         return super()._add_chat_ids(chat_id)
 
-    def remove_chat_ids(self, chat_id: SLT[int]) -> None:
+    def remove_chat_ids(self, chat_id: SCT[int]) -> None:
         """
         Remove one or more chats from allowed chat ids.
 
         Args:
-            chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which chat/user ID(s) to
+            chat_id(:obj:`int` | Collection[:obj:`int`]): Which chat/user ID(s) to
                 disallow through.
         """
         return super()._remove_chat_ids(chat_id)
@@ -1428,7 +1428,7 @@ class Language(MessageFilter):
         ``MessageHandler(filters.Language("en"), callback_method)``
 
     Args:
-        lang (:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]):
+        lang (:obj:`str` | Collection[:obj:`str`]):
             Which language code(s) to allow through.
             This will be matched using :obj:`str.startswith` meaning that
             'en' will match both 'en_US' and 'en_GB'.
@@ -1437,7 +1437,7 @@ class Language(MessageFilter):
 
     __slots__ = ("lang",)
 
-    def __init__(self, lang: SLT[str]):
+    def __init__(self, lang: SCT[str]):
         if isinstance(lang, str):
             lang = cast(str, lang)
             self.lang = [lang]
@@ -1596,9 +1596,9 @@ class SenderChat(_ChatUserBaseFilter):
         the current set of allowed chats.
 
     Args:
-        chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional):
+        chat_id(:obj:`int` | Collection[:obj:`int`], optional):
             Which sender chat chat ID(s) to allow through.
-        username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional):
+        username(:obj:`str` | Collection[:obj:`str`], optional):
             Which sender chat username(s) to allow through.
             Leading ``'@'`` s in usernames will be discarded.
         allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no sender
@@ -1638,12 +1638,12 @@ def filter(self, message: Message) -> bool:
     CHANNEL = _CHANNEL(name="filters.SenderChat.CHANNEL")
     """Messages whose sender chat is a channel."""
 
-    def add_chat_ids(self, chat_id: SLT[int]) -> None:
+    def add_chat_ids(self, chat_id: SCT[int]) -> None:
         """
         Add one or more sender chats to the allowed chat ids.
 
         Args:
-            chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which sender chat ID(s) to
+            chat_id(:obj:`int` | Collection[:obj:`int`]): Which sender chat ID(s) to
                 allow through.
         """
         return super()._add_chat_ids(chat_id)
@@ -1651,12 +1651,12 @@ def add_chat_ids(self, chat_id: SLT[int]) -> None:
     def get_chat_or_user(self, message: Message) -> Optional[TGChat]:
         return message.sender_chat
 
-    def remove_chat_ids(self, chat_id: SLT[int]) -> None:
+    def remove_chat_ids(self, chat_id: SCT[int]) -> None:
         """
         Remove one or more sender chats from allowed chat ids.
 
         Args:
-            chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which sender chat ID(s) to
+            chat_id(:obj:`int` | Collection[:obj:`int`]): Which sender chat ID(s) to
                 disallow through.
         """
         return super()._remove_chat_ids(chat_id)
@@ -2103,9 +2103,9 @@ class User(_ChatUserBaseFilter):
         ``MessageHandler(filters.User(1234), callback_method)``
 
     Args:
-        user_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): Which user ID(s) to
+        user_id(:obj:`int` | Collection[:obj:`int`], optional): Which user ID(s) to
             allow through.
-        username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional):
+        username(:obj:`str` | Collection[:obj:`str`], optional):
             Which username(s) to allow through. Leading ``'@'`` s in usernames will be discarded.
         allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user is
             specified in :attr:`user_ids` and :attr:`usernames`. Defaults to :obj:`False`.
@@ -2122,8 +2122,8 @@ class User(_ChatUserBaseFilter):
 
     def __init__(
         self,
-        user_id: SLT[int] = None,
-        username: SLT[str] = None,
+        user_id: SCT[int] = None,
+        username: SCT[str] = None,
         allow_empty: bool = False,
     ):
         super().__init__(chat_id=user_id, username=username, allow_empty=allow_empty)
@@ -2150,25 +2150,25 @@ def user_ids(self) -> FrozenSet[int]:
         return self.chat_ids
 
     @user_ids.setter
-    def user_ids(self, user_id: SLT[int]) -> None:
+    def user_ids(self, user_id: SCT[int]) -> None:
         self.chat_ids = user_id  # type: ignore[assignment]
 
-    def add_user_ids(self, user_id: SLT[int]) -> None:
+    def add_user_ids(self, user_id: SCT[int]) -> None:
         """
         Add one or more users to the allowed user ids.
 
         Args:
-            user_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which user ID(s) to allow
+            user_id(:obj:`int` | Collection[:obj:`int`]): Which user ID(s) to allow
                 through.
         """
         return super()._add_chat_ids(user_id)
 
-    def remove_user_ids(self, user_id: SLT[int]) -> None:
+    def remove_user_ids(self, user_id: SCT[int]) -> None:
         """
         Remove one or more users from allowed user ids.
 
         Args:
-            user_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which user ID(s) to
+            user_id(:obj:`int` | Collection[:obj:`int`]): Which user ID(s) to
                 disallow through.
         """
         return super()._remove_chat_ids(user_id)
@@ -2203,9 +2203,9 @@ class ViaBot(_ChatUserBaseFilter):
         ``MessageHandler(filters.ViaBot(1234), callback_method)``
 
     Args:
-        bot_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): Which bot ID(s) to
+        bot_id(:obj:`int` | Collection[:obj:`int`], optional): Which bot ID(s) to
             allow through.
-        username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional):
+        username(:obj:`str` | Collection[:obj:`str`], optional):
             Which username(s) to allow through. Leading ``'@'`` s in usernames will be
             discarded.
         allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user
@@ -2223,8 +2223,8 @@ class ViaBot(_ChatUserBaseFilter):
 
     def __init__(
         self,
-        bot_id: SLT[int] = None,
-        username: SLT[str] = None,
+        bot_id: SCT[int] = None,
+        username: SCT[str] = None,
         allow_empty: bool = False,
     ):
         super().__init__(chat_id=bot_id, username=username, allow_empty=allow_empty)
@@ -2251,25 +2251,25 @@ def bot_ids(self) -> FrozenSet[int]:
         return self.chat_ids
 
     @bot_ids.setter
-    def bot_ids(self, bot_id: SLT[int]) -> None:
+    def bot_ids(self, bot_id: SCT[int]) -> None:
         self.chat_ids = bot_id  # type: ignore[assignment]
 
-    def add_bot_ids(self, bot_id: SLT[int]) -> None:
+    def add_bot_ids(self, bot_id: SCT[int]) -> None:
         """
         Add one or more bots to the allowed bot ids.
 
         Args:
-            bot_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which bot ID(s) to allow
+            bot_id(:obj:`int` | Collection[:obj:`int`]): Which bot ID(s) to allow
                 through.
         """
         return super()._add_chat_ids(bot_id)
 
-    def remove_bot_ids(self, bot_id: SLT[int]) -> None:
+    def remove_bot_ids(self, bot_id: SCT[int]) -> None:
         """
         Remove one or more bots from allowed bot ids.
 
         Args:
-            bot_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): Which bot ID(s) to
+            bot_id(:obj:`int` | Collection[:obj:`int`], optional): Which bot ID(s) to
                 disallow through.
         """
         return super()._remove_chat_ids(bot_id)


Test patch: