In this tutorial I will go through the entire length of pagination in discord.py
I kindly ask you to go through the entire thing, as it is not recommended to skip the subtopics because they are interconnected in some way.
Pagination is an extremely common thing to do in discord.py that I decided to create this gist. One of the most common uses of pagination is when you want to show contents that are more than the limit of what discord allows you.
In this tutorial, you would need to install discord-ext-menus
library that was written by Danny. The creator of discord.py. This
library mainly uses reactions as interfaces. Now don't worry, we
will go into discord button once we fully understand this library.
Before diving any further, make sure to install discord-ext-menus.
While the library contains more than just pagination. We will mainly go into pagination because that's the focus.
The library contains a few classes, mainly these ones.
menus.Menumenus.MenuPagesmenus.ListPageSourcemenus.GroupByPageSourcemenus.AsyncIteratorPageSource
However, we will only look into 3 of them, which is menus.Menu
, menus.MenuPages and menus.ListPageSource. The rest are derived from
menus.MenuPages which you can learn on your own after this tutorial.
This class is responsible for handling the reactions given and the behaviour of what a reaction would do.
Let's use the example given by the library.
from discord.ext import menus
class MyMenu(menus.Menu):
async def send_initial_message(self, ctx, channel):
return await channel.send(f'Hello {ctx.author}')
@menus.button('\N{THUMBS UP SIGN}')
async def on_thumbs_up(self, payload):
await self.message.edit(content=f'Thanks {self.ctx.author}!')
@menus.button('\N{THUMBS DOWN SIGN}')
async def on_thumbs_down(self, payload):
await self.message.edit(content=f"That's not nice {self.ctx.author}...")
@menus.button('\N{BLACK SQUARE FOR STOP}\ufe0f')
async def on_stop(self, payload):
self.stop()
@bot.command()
async def your_command(ctx):
menu = MyMenu()
await menu.start(ctx)send_initial_messagemethod is called when you callmenu.start. You're required to return a message object forMyMenuto handle them.menus.buttonrefers to your reaction. They are automatically reacted by your bot as soon asmenu.startis called. The first argument would be the emoji that you would want to listen to and added to the Message.- The callbacks of each decorator of
menus.buttonare called when you reacted to the corresponding reaction in the decorator. self.stopmethod stops theMyMenuclass from listening to the reactions. This fully stops the instance from operating.
To simply, this sums up on what menus.Menu class does.
Of course, it's a lot more complicated than this but this is the simplification i can give you.
As you can see, you can do plenty of things with this class. There's lots of application that can be used from this class alone.
For example, you can do a confirmation button using pure reactions. Have a greater control in controlling reactions without any headache. You can also use this for controls in a game where it is purely based on reactions. While yes, discord button exist. This were made before the existence of discord buttons.
It's great to have a fundamental understanding of how this will help us on creating a pagination based on reactions AND discord buttons.
[Reaction based pagination]
This class is responsible for controlling the reactions for pagination. It is responsible for handling the reactions properly on what to do whether to show the first page, next page and so on.
This class inherits menus.Menu. Meaning, all functionality coming from menus.Menu is also available
in this class. This class fully has all the methods to control a pagination.
However, this class alone is entirely useless. To make it useful, we will need
ListPageSource.
This class is responsible for formatting each page in our paginator. It is also responsible for handling each of our data. Given an iterable into the class would allow it to fully handle the data into each pages. We will see this in action in Combine MenuPages & ListPageSource
With the help of MenuPages as our reaction manager and ListPageSource as our data and format
manager. We can fully make an operational paginator with them.
from discord.ext import menus
class MySource(menus.ListPageSource):
async def format_page(self, menu, entries):
return f"This is number {entries}."
@bot.command()
async def your_command(ctx):
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
formatter = MySource(data, per_page=1)
menu = menus.MenuPages(formatter)
await menu.start(ctx)MySourceacts as a formatter, givendata, elements will be separated given byper_pagekwargs. In this case, each element is separated as a single page.MenuPagesaccepts any class that subclassesmenus.PageSource. This includesListPageSource(ourMySource).MenuPagesadds all the necessary reactions for navigation.- everytime
MenuPagesreceives a reaction, it processes them on which way to go. After that, it callsformat_pageafter it processed on which page to show. - When
format_pageis called.menuwould beMenuPagesinstance andentrieswill be the values that were separated given byper_pagefromdata. - Anything that is returned in
format_pageare displayed onto your Message. The value that can be returned areEmbed/dict/str.
If you want to customize the reaction. You would need to override MenuPages class. As we know,
MenuPages is the class that handles the reaction. We can fully change this to fit our needs.
import discord
from discord.ext import menus
from discord.ext.menus import button, First, Last
class MyMenuPages(menus.MenuPages, inherit_buttons=False):
@button('<:before_fast_check:754948796139569224>', position=First(0))
async def go_to_first_page(self, payload):
await self.show_page(0)
@button('<:before_check:754948796487565332>', position=First(1))
async def go_to_previous_page(self, payload):
await self.show_checked_page(self.current_page - 1)
@button('<:next_check:754948796361736213>', position=Last(1))
async def go_to_next_page(self, payload):
await self.show_checked_page(self.current_page + 1)
@button('<:next_fast_check:754948796391227442>', position=Last(2))
async def go_to_last_page(self, payload):
max_pages = self._source.get_max_pages()
last_page = max(max_pages - 1, 0)
await self.show_page(last_page)
@button('<:stop_check:754948796365930517>', position=Last(0))
async def stop_pages(self, payload):
self.stop()
class MySource(menus.ListPageSource):
async def format_page(self, menu, entries):
embed = discord.Embed(
description=f"This is number {entries}.",
color=discord.Colour.random()
)
embed.set_footer(text=f"Requested by {menu.ctx.author}")
return embed
@bot.command()
async def your_command(ctx):
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
formatter = MySource(data, per_page=1)
menu = MyMenuPages(formatter)
await menu.start(ctx)- Setting
inherit_buttonskwargs toFalseremoves buttons that came fromMenuPages. - in each
buttondecorator, the first argument would be your emoji. Followed bypositionkwargs.positionkwargs acceptsPosition,First,Lastclass. Firstrefers to your position of button. This class inheritsPosition.Firstacts as an anchor, where it will always be added beforeLastclass gets added. In this case, the position of buttons would be;First(0),First(1),Last(0),Last(1),Last(2)
show_pagemethod sets thecurrent_pageto your page. In this case, it is used ingo_to_first_pagewhere we only wanna show the first page, andgo_to_last_pagewhere we only want to show the last page.show_checked_pagemethod usesshow_page, but checks if it is within the range of your page. This ensure that there isn't an out of bound exception.format_pagethis time returns an embed, with a randomized color to make it pretty.- We would use
MyMenuPagesinstead ofMenuPagesbecause we want to try the custom emoji. - The rest of the explanation are available at Menu
As a reference, here are the emojis I'm using currently.
As you can see, it is relatively easy to create your own pagination with the default MenuPages. The complexity increases as you want to start doing customization, however this can be easy once you get the hang of it.
It is recommended for you to explore other classes for other use cases. However, I'm not gonna be focusing them here.
Now that we have the fundamentals of menus. We can also construct them using View on our own ways.
[Button based pagination]
This tutorial is only compatible with discord.py 2.0. Any lower such as discord.py 1.7 will not have this class because it was not built with the newer feature of discord such as buttons and dropdowns.
You would need to install discord.py 2.0 via git instead of pypi. 2.0 were never released thus not available in pypi.
This class is responsible for handling classes that inherits from discord.ui.Item. This includes discord.ui.Button which we will be using in this tutorial as navigation for the pagination.
Here are a list of classes that is related to View.
discord.ui.Viewdiscord.ui.Itemdiscord.ui.Buttondiscord.ui.Select
However, we will only talk about 2 here which is discord.ui.View and discord.ui.Button. Those are the only thing we
need for the pagination.
To start, discord.ui.View works similarly like menus.Menu. To demonstrate, here's an example.
import discord
from discord import ui
class MyView(ui.View):
@ui.button(label="Hello", emoji="\U0001f590", style=discord.ButtonStyle.blurple)
async def on_click_hello(self, button, interaction):
await interaction.response.send_message("Hi")
@bot.command()
async def your_command(ctx):
view = MyView()
await ctx.send("Click button", view=view)ui.buttonis a decorator that will create a Button. The parameter are almost the same as discord.ui.Button.- The said decorator will call the callback when clicked which in this case refers to
on_click_hellomethod. It will passbuttonwhich isdiscord.ui.Buttonandinteractionwhich isdiscord.Interactionas the parameter. interaction.responsereturnsInteractionResponseinstance, which you can use to respond to the user. We will usesend_messagefor now to send a message.MyViewis instantiatedabc.Messageable.sendcontainsviewkwargs which you can pass yourViewinstance into. In our case,viewis the instance ofMyView.
With this brief knowledge, we can now make pagination with View. As we know, MenuPages acts as the navigation of
the pagination while ListPageSource acts as the formatter which format the data and the page. While menus.Menu handles
reactions and menus.MenuPages contains everything about pagination handling that we really needed. We only need
the handling part.
Based on this knowledge alone, we can combine MenuPages with discord.ui.View to make a fully functioning paginator
with discord.ui.Button as the navigation. This code will be similar to Custom MenuPages code.
import discord
from discord import ui
from discord.ext import menus
class MyMenuPages(ui.View, menus.MenuPages):
def __init__(self, source):
super().__init__(timeout=60)
self._source = source
self.current_page = 0
self.ctx = None
self.message = None
async def start(self, ctx, *, channel=None, wait=False):
# We wont be using wait/channel, you can implement them yourself. This is to match the MenuPages signature.
await self._source._prepare_once()
self.ctx = ctx
self.message = await self.send_initial_message(ctx, ctx.channel)
async def _get_kwargs_from_page(self, page):
"""This method calls ListPageSource.format_page class"""
value = await super()._get_kwargs_from_page(page)
if 'view' not in value:
value.update({'view': self})
return value
async def interaction_check(self, interaction):
"""Only allow the author that invoke the command to be able to use the interaction"""
return interaction.user == self.ctx.author
# This is extremely similar to Custom MenuPages(I will not explain these)
@ui.button(emoji='<:before_fast_check:754948796139569224>', style=discord.ButtonStyle.blurple)
async def first_page(self, button, interaction):
await self.show_page(0)
@ui.button(emoji='<:before_check:754948796487565332>', style=discord.ButtonStyle.blurple)
async def before_page(self, button, interaction):
await self.show_checked_page(self.current_page - 1)
@ui.button(emoji='<:stop_check:754948796365930517>', style=discord.ButtonStyle.blurple)
async def stop_page(self, button, interaction):
self.stop()
@ui.button(emoji='<:next_check:754948796361736213>', style=discord.ButtonStyle.blurple)
async def next_page(self, button, interaction):
await self.show_checked_page(self.current_page + 1)
@ui.button(emoji='<:next_fast_check:754948796391227442>', style=discord.ButtonStyle.blurple)
async def last_page(self, button, interaction):
await self.show_page(self._source.get_max_pages() - 1)
@bot.command()
async def your_command(ctx):
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
formatter = MySource(data, per_page=1) # MySource came from Custom MenuPages subtopic. [Please refer to that]
menu = MyMenuPages(formatter)
await menu.start(ctx)Jesus christ that's a long code
- We inherit both
ui.Viewandmenus.MenuPages. Where we will borrow methods frommenus.MenuPageswhile usingui.Viewas our main navigation for the pagination. - In
__init__, thesuper().__init__will refer toui.Viewinstead. To understand why this is, learn Method Resolution Order(MRO).sourceargument must be aPageSourceinstance. The rest of the attribute are required to be assign becauseMenuPageswill use them. - In
startmethod, it will follow theMenuPagesmethod signature.self.send_initial_messageis a method fromMenuPages, it acts as sending a message to the user and returns a Message object. We will store them inself.message self._source._prepare_onceis a method to declare that thePageSourceobject has started._get_kwargs_from_pagemethod is also fromMenuPages, it is responsible for callingformat_pageand returns a dictionary which will directly be used indiscord.Message.editkwargs. We take advantage of this and putviewas the to put ourViewobject into the message for navigation.interaction_checkis a method ofui.View. It gets called when a button is clicked. ReturnTrueto allow for callback to be called. We will useinteraction.userwhere it's the person who clicked the button to check if they are our author. If it is, it's True else return False. Optionally, you would send an ephemaral message to the user if it is not the author.- The rest of the explanation must refer to Custom MenuPages subtopic. It is exactly the same explanation.
While yes it may seem long. But keep in mind, you would only do MyMenuPages once, after that, you can create infinite
amount of ListPageSource that fit your need for all of the pagination you will ever need. Feel free to derive this code
into a much more advance handler. I've only talked briefly on how to use ui.View. There's plenty more uses you can do
with it and I would recommend exploring more on ui.View. Danny has created plenty of View examples here: Click Here.
Congrats, you've reached the end of this tutorial. I don't recommend skipping any of the topic here because you will be
left confused on what you've just read and learnt nothing. Each subtopic is linked to each other hence why. Anyways, any
question regarding ui.View and discord-ext-menus can be asked in discord.py. I don't check this gist that much. So your
question wont be answered that quickly.
Tbh i wrote this because i need to remind myself about this in about a year. that's all, thanks.






