Skip to content

Instantly share code, notes, and snippets.

@InterStella0
Last active December 17, 2025 09:13
Show Gist options
  • Select an option

  • Save InterStella0/b78488fb28cadf279dfd3164b9f0cf96 to your computer and use it in GitHub Desktop.

Select an option

Save InterStella0/b78488fb28cadf279dfd3164b9f0cf96 to your computer and use it in GitHub Desktop.
Walkthrough guide on subclassing HelpCommand

A basic walkthrough guide on subclassing HelpCommand

This guide will walkthrough the ways to create a custom help command by subclassing HelpCommand.

Table Content

Brief explanation of subclassing

In simple terms, a subclass is a way to inherit a class behaviour/attributes from another class. Here's how you would subclass a class in Python.

class A:
    def __init__(self, attribute1):
        self.attribute1 = attribute1

    def method1(self):
        print("method 1")

    def method2(self):
        print("method 2")


class B(A):
    def __init__(self, attribute1, attribute2):
        super().__init__(attribute1) # This calls A().__init__ magic method.
        self.attribute2 = attribute2

    def method1(self): # Overrides A().method1
        print("Hi")

Given the example, the variable instance_a contains the instance of class A. As expected, the output will be this.

>>> instance_a = A(1)
>>> instance_a.attribute1
1
>>> instance_a.method1()
"method 1"

How about B class? instance_b contains the instance of class B, it inherits attributes/methods from class A. Meaning it will have everything that class A have, but with an additional attribute/methods.

>>> instance_b = B(1, 2)
>>> instance_b.attribute1
1
>>> instance_b.attribute2
2
>>> instance_b.method1()
"Hi"
>>> instance_b.method2()
"method 2"

Make sure to look and practice more into subclassing classes to understand fully on what it is before diving into subclassing HelpCommand.

Why subclassing HelpCommand is better

Firstly, let me show you the wrong way of creating a help command.

One of the most incorrect ways of to create a help command.

bot = commands.Bot(command_prefix="uwu ", help_command=None)

# OR
bot.help_command = None

# OR
bot.remove_command("help")

@bot.command()
async def help(ctx):
    ...

This is directly from YouTube, which are known to have bad tutorials for discord.py.

Why are these bad?

1. Command handling specifically for HelpCommand

Missing out on command handling that are specifically for HelpCommand.

For instance, say my prefix is !. Command handling such as

!help

!help <command>

!help <group>

!help <cog>

For a HelpCommand, all of these are handled in the background, including showing appropriate error when command/group/cog are an invalid argument that were given. You can also show custom error when an invalid argument are given.

For people who remove the HelpCommand? There is no handling, you have to do it yourself.

For example

Bad way
bot = commands.Bot(command_prefix="!", help_command=None)

@bot.command()
async def help(ctx, argument=None):
    # !help
    if argument is None:
        await ctx.send("This is help")
    
    elif argument in bot.all_commands:
        command = bot.get_command(argument)
        if isinstance(command, commands.Group):
            # !help <group>
           await ctx.send("This is help group")
        else:
            # !help <command>
           await ctx.send("This is help command")
    elif argument in bot.cogs:
        # !help <cog>
        cog = bot.get_cog(argument)
        await ctx.send("This is help cog")
    else:
       await ctx.send("Invalid command or cog")

This is an ok implementation and all, but you have to handle more than this. I'm only simplifying the code.

Now for the subclassed HelpCommand code.

Good way

bot = commands.Bot(command_prefix="!")

class MyHelp(commands.HelpCommand):
   # !help
    async def send_bot_help(self, mapping):
        await self.context.send("This is help")
       
   # !help <command>
    async def send_command_help(self, command):
        await self.context.send("This is help command")
      
   # !help <group>
    async def send_group_help(self, group):
        await self.context.send("This is help group")
    
   # !help <cog>
    async def send_cog_help(self, cog):
        await self.context.send("This is help cog")

bot.help_command = MyHelp()

Not only does HelpCommand looks better, it is also much more readable compared to the bad way. Oh, did I mention that HelpCommand also handles invalid arguments for you? Yeah. It does.

2. Utilities

HelpCommand contains a bunch of useful methods you can use in order to assist you in creating your help command and formatting.

Methods / Attributes Usage
HelpCommand.filter_commands() Filter commands to only show commands that the user can run. This help hide any secret commands from the general user.
HelpCommand.clean_prefix Get a clean prefix that escape mentions and format them in a readable way such as @Name instead of <@id> format. Works up to version 1.7.1.
Context.clean_prefix HelpCommand.clean_prefix was removed in version 2.0 of discord.py and replaced with Context.clean_prefix
HelpCommand.get_command_signature() Get the command signature and format them such as command [argument] for optional and command <argument> for required.
HelpCommand.prepare_help_command() Triggers before every send_x_help method are triggered, this work exactly like command.before_invoke
HelpCommand.get_bot_mapping() Get all command that are available in the bot, sort them by Cogs and None for No Category as key in a dictionary. This method is triggered before HelpCommand.send_bot_help is triggered, and will get passed as the parameter.
HelpCommand.get_destination() Returns a Messageable on where the help command was invoked.
HelpCommand.command_callback The method that handles all help/help cog/ help command/ help group and call which method appropriately. This is useful if you want to modify the behavour of this. Though more knowledge is needed for you to do that. Most don't use this.
Context.send_help() Calling send_command_help based on what the Context command object were. This is useful to be used when the user incorrectly invoke the command. Which you can call this method to show help quickly and efficiently. (Only works if you have HelpCommand configured)

All of this does not exist when you set bot.help_command to None. You miss out on this.

3. Modular/Dynamic

Since it's a class, most people would make it modular. They put it in a cog for example. There is a common code given in discord.py created by Vex.

class MyHelpCommand(commands.MinimalHelpCommand):
    def get_command_signature(self, command):
        return '{0.clean_prefix}{1.qualified_name} {1.signature}'.format(self, command)

class MyCog(commands.Cog):
    def __init__(self, bot):
        self._original_help_command = bot.help_command
        bot.help_command = MyHelpCommand()
        bot.help_command.cog = self
        
    def cog_unload(self):
        self.bot.help_command = self._original_help_command

What does this mean?

Well, first we have a HelpCommand made there called MyHelpCommand. When MyCog class is loaded, bot.help_command is stored into self._original_help_command. This preserve the old help command that was attached to the bot, and then it is assigned to a new HelpCommand that you've made.

cog_unload is triggered when the cog is unloaded, which assign bot.help_command to the original help command.

What good does this give?

For example, you have a custom help command that is currently attached to bot.help_command. But you want to develop a new help command or modify the existing without killing the bot. So you can just unload the cog, which will assign the old help command to the bot so that you will always have a backup HelpCommand ready while you're modifying and testing your custom help.

Getting started

With that out of the way, let's get started. For subclassing HelpCommand, first, you would need to know the types of HelpCommand. Where each class has their own usage.

Types of HelpCommand class

There are a few types of HelpCommand classes that you can choose;

  1. DefaultHelpCommand a help command that is given by default.
  2. MinimalHelpCommand a slightly better help command.
  3. HelpCommand an empty class that is the base class for every HelpCommand you see. On its own, it will not do anything.

By default, help command is using the class DefaultHelpCommand. This is stored in bot.help_command. This attribute will ONLY accept instances that subclasses HelpCommand. Here is how you were to use the DefaultHelpCommand instance.

from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")
bot.help_command = commands.DefaultHelpCommand()

# OR

bot = commands.Bot(command_prefix="uwu ", help_command=commands.DefaultHelpCommand())
# Both are equivalent

Here's an example of what that looks like.

img.png Now, of course, this is done by default. I'm only showing you this as a demonstration. Don't scream at me

Let's do the same thing with MinimalHelpCommand next.

from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")
bot.help_command = commands.MinimalHelpCommand()

This is how that would look like:

minhelpcommand.png

Embed MinimalHelpCommand

Now say, you want the content to be inside an embed. But you don't want to change the content of DefaultHelpCommand/MinimalHelpCommand since you want a simple HelpCommand with minimal work. There is a short code from ?tag embed help example by gogert in discord.py server, a sample code you can follow shows this;

import discord
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")

class MyNewHelp(commands.MinimalHelpCommand):
    async def send_pages(self):
        destination = self.get_destination()
        for page in self.paginator.pages:
            emby = discord.Embed(description=page)
            await destination.send(embed=emby)

bot.help_command = MyNewHelp()

The resulting code will show that it have the content of MinimalHelpCommand but in an embed.

embedminimalhelp.png

How does this work?

Looking over the MinimalHelpCommand source code, here. Every method that is responsible for <prefix>help <argument> will call MinimalHelpCommand.send_pages when it is about to send the content. This makes it easy to just override send_pages without having to override any other method there are in MinimalHelpCommand.

HelpCommand

Basic methods to override

If you want to use HelpCommand class, we need to understand the basic of subclassing HelpCommand. Here are a list of HelpCommand relevant methods, and it's responsibility.

  1. HelpCommand.send_bot_help(mapping) Gets called with <prefix>help
  2. HelpCommand.send_command_help(command) Gets called with <prefix>help <command>
  3. HelpCommand.send_group_help(group) Gets called with <prefix>help <group>
  4. HelpCommand.send_cog_help(cog) Gets called with <prefix>help <cog>

Useful attributes

  1. HelpCommand.context the Context object in the help command.
  2. HelpCommand.clean_prefix a cleanup prefix version that remove any mentions.

For more, Click here

HelpCommand Flowchart

This is a bare minimum on what you should know on how a HelpCommand operate. As of discord version 1.* and 2.0. It remained the same flow. helpcommandflowchart.png

Seems simple enough? Now let's see what happens if you override one of the methods. Here's an example code of how you would do that. This override will say "hello!" when you type <prefix>help to demonstrate on what's going on.

We'll use HelpCommand.get_destination() to get the abc.Messageable instance for sending a message to the correct channel.

Code Example

from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")

class MyHelp(commands.HelpCommand):
    async def send_bot_help(self, mapping):
        channel = self.get_destination()
        await channel.send("hello!")

bot.help_command = MyHelp()

Output

hellohelpcommand.png

Keep in mind, using HelpCommand class will require overriding every send_x_help methods. For example, <prefix>help jsk is a command that should call send_command_help method. However, since HelpCommand is an empty class, it will not say anything.

help command

Let's work our way to create a <prefix> help. Given the documentation, await send_bot_help(mapping) method receives mapping(Mapping[Optional[Cog], List[Command]]) as its parameter. await indicates that it should be an async function.

What does this mean?

  • Mapping[] is a collections.abc.Mapping, for simplicity’s sake, this usually refers to a dictionary since it's under collections.abc.Mapping.
  • Optional[Cog] is a Cog object that has a chance to be None.
  • List[Command] is a list of Command objects.
  • Mapping[Optional[Cog], List[Command]] means it's a map object with Optional[Cog] as it's key and List[Command] as its value.

All of these are typehints in the typing module. You can learn more about it here.

Now, for an example, we will use this mapping given in the parameter of send_bot_help. For each of the command, we'll use HelpCommand.get_command_signature(command) to get the command signature of a command in an str form.

Example Code

import discord
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")

class MyHelp(commands.HelpCommand):
    async def send_bot_help(self, mapping):
        embed = discord.Embed(title="Help")
        for cog, commands in mapping.items():
           command_signatures = [self.get_command_signature(c) for c in commands]
           if command_signatures:
                cog_name = getattr(cog, "qualified_name", "No Category")
                embed.add_field(name=cog_name, value="\n".join(command_signatures), inline=False)

        channel = self.get_destination()
        await channel.send(embed=embed)

bot.help_command = MyHelp()

How does it work?

  1. Create an embed.
  2. Use dict.items() to get an iterable of (Cog, list[Command]).
  3. Each element in list[Command], we will call self.get_command_signature(command) to get the proper signature of the command.
  4. If the list is empty, meaning, no commands is available in the cog, we don't need to show it, hence if command_signatures:.
  5. cog has a chance to be None, this refers to No Category. We'll use getattr to avoid getting an error to get cog's name through Cog.qualified_name.
  6. Using str.join each command will be displayed on a separate line.
  7. Once all of this is finished, display it.

The result

samplehelp.png

I agree, this does not look pretty. But this demonstrate how to create a help command with minimal effort. The reason why it looks ugly, is that we're using HelpCommand.get_command_signature(command). Let's override that method to make it a little more readable.

We'll borrow codes from MinimalHelpCommand.get_command_signature. Optionally, we can subclass MinimalHelpCommand instead of copying codes. I'm doing this as a demonstration of overriding other methods.

We'll also use HelpCommand.filter_commands, this method will filter commands by removing any commands that the user cannot use. It is a handy method to use.

The Example

import discord
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")

class MyHelp(commands.HelpCommand):
   def get_command_signature(self, command):
        return '%s%s %s' % (self.clean_prefix, command.qualified_name, command.signature)

    async def send_bot_help(self, mapping):
        embed = discord.Embed(title="Help")
        for cog, commands in mapping.items():
           filtered = await self.filter_commands(commands, sort=True)
           command_signatures = [self.get_command_signature(c) for c in filtered]
           if command_signatures:
                cog_name = getattr(cog, "qualified_name", "No Category")
                embed.add_field(name=cog_name, value="\n".join(command_signatures), inline=False)

        channel = self.get_destination()
        await channel.send(embed=embed)

bot.help_command = MyHelp()

The resulting output

betterhelpcommand.png

This looks more readable than the other one. While this should cover most of your needs, you may want to know more helpful attribute that is available on HelpCommand in the official documentation.

help [argument] command

Now that the hard part is done, let's take a look at <prefix>help [argument]. The method responsible for this is as follows;

  1. send_command_help
  2. send_cog_help
  3. send_group_help

As a demonstration, let's go for send_command_help this method receive a Command object. For this, it's simple, all you have show is the attribute of the command.

For example, this is your command code, your goal is you want to show the help,aliases and the signature.

Command Code

@bot.command(help="Shows all bot's command usage in the server on a sorted list.",
             aliases=["br", "brrrr", "botranks", "botpos", "botposition", "botpositions"])
async def botrank(ctx, bot: discord.Member):
   pass

Then it's simple, you can display each of the attribute by Command.help and Command.aliases.

For the signature, instead of using the previous get_command_signature, we're going to subclass MinimalHelpCommand.

Help Code

import discord
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")

class MyHelp(commands.MinimalHelpCommand):
    async def send_command_help(self, command):
        embed = discord.Embed(title=self.get_command_signature(command))
        embed.add_field(name="Help", value=command.help)
        alias = command.aliases
        if alias:
            embed.add_field(name="Aliases", value=", ".join(alias), inline=False)

        channel = self.get_destination()
        await channel.send(embed=embed)

bot.help_command = MyHelp()

What you get

commandhelpcommand.png

As you can see, it is very easy to create <prefix>help [argument]. The class already handles the pain of checking whether the given argument is a command, a cog, or a group command. It's up to you on how you want to display it, whether it's through a plain message, an embed or even using discord.ext.menus.

Command Attributes

Let's say, someone is spamming your help command. For a normal command, all you have to do to combat this is using a cooldown decorator and slap that thing above the command declaration. Or, what about if you want an alias? Usually, you would put an aliases kwargs in the command decorator. However, HelpCommand is a bit special, It's a god damn class. You can't just put a decorator on it and expect it to work.

That is when HelpCommand.command_attrs come to the rescue. This attribute can be set during the HelpCommand declaration, or a direct attribute assignment. According to the documentation, it accepts exactly the same thing as a command decorator in a form of a dictionary.

For example, we want to rename the help command as "hell" instead of "help" for whatever reason. We also want to make an alias for "help" so users can call the command with "hell" and "help". Finally, we want to put a cooldown, because help command messages are big, and we don't want people to spam those. So, what would the code look like?

Example Code

from discord.ext import commands

attributes = {
   'name': "hell",
   'aliases': ["help", "helps"],
   'cooldown': commands.Cooldown(2, 5.0, commands.BucketType.user)
} 
# For 2.0, you would use CooldownMapping.from_cooldown(rate, per, type)
# because Cooldown no longer have type as it's arguments.

# During declaration
help_object = commands.MinimalHelpCommand(command_attrs=attributes)

# OR through attribute assignment
help_object = commands.MinimalHelpCommand()
help_object.command_attrs = attributes

bot = commands.Bot(command_prefix="uwu ", help_command=help_object)

How does it work?

  1. sets the name into "hell" is refers to here 'name': "hell".
  2. sets the aliases by passing the list of str to the aliases key, which refers to here 'aliases': ["help", "helps"].
  3. sets the cooldown through the "cooldown" key by passing in a Cooldown object. This object will make a cooldown with a rate of 2, per 5 with a bucket type BucketType.user, which in simple terms, for every discord.User, they can call the command twice, every 5 seconds.
  4. We're going to use MinimalHelpCommand as the HelpCommand object.

Note: on Number 3, Cooldown has been updated on 2.0. Please check the code on your own instead.

The result

cooldownhelp.png

As you can see, the name of the help command is now "hell", and you can also trigger the help command by "help". It will also raise an OnCommandCooldown error if it was triggered 3 times in 5 seconds due to our Cooldown object. Of course, I didn't show that in the result, but you can try it yourself. You should handle the error in an error handler when an OnCommandCooldown is raised.

Error handling for HelpCommand

Basic error message override

What happens when <prefix>help command fails to get a command/cog/group? Simple, HelpCommand.send_error_message will be called. HelpCommand will not call on_command_error when it can't find an existing command. It will also give you an str instead of an error instance.

import discord
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")

class MyHelp(commands.HelpCommand):
    async def send_error_message(self, error):
        embed = discord.Embed(title="Error", description=error)
        channel = self.get_destination()
        await channel.send(embed=embed)

bot.help_command = MyHelp()

error is a string that will only contain the message, all you have to do is display the message.

The output:

errorhelp.png

How about a local error handler?

Indeed, we have it. HelpCommand.on_help_command_error, this method is responsible for handling any error just like any other local error handler.

Code

import discord
from discord.ext import commands
bot = commands.Bot(command_prefix="uwu ")

class MyHelp(commands.HelpCommand):
    async def send_bot_help(self, mapping):
        raise commands.BadArgument("Something broke")

    async def on_help_command_error(self, ctx, error):
        if isinstance(error, commands.BadArgument):
            embed = discord.Embed(title="Error", description=str(error))
            await ctx.send(embed=embed)
        else:
            raise error

bot.help_command = MyHelp()

Rather basic, just raise an error that subclasses commands.CommandError such as commands.BadArgument. The error raised will cause on_help_command_error to be invoked. The code shown will catch this commands.BadArgument instance that is stored in error variable, and show the message.

Output:

errorhandler.png

To be fair, you should create a proper error handler through this official documentation. Here.

There is also a lovely example by Mysty on error handling in general. Here. This example shows how to properly create a global error handler and local error handler.

Setting Cog for HelpCommand

Super easy lol

It works just like setting a cog on a Command object, you basically have to assign a commands.Cog instance into HelpCommand.cog. It is pretty common for discord.py users to put a HelpCommand into a cog/separate file since people want organization.

Code Example

This code example is if you're in a Cog file, which you have access to a Cog instance

from discord.ext import commands

# Unimportant part
class MyHelp(commands.HelpCommand):
    async def send_bot_help(self, mapping):
        channel = self.get_destination()
        await channel.send("hey")


class YourCog(commands.Cog):
    def __init__(self, bot):
       self.bot = bot
        
       # Focus here
       # Setting the cog for the help
       help_command = MyHelp()
       help_command.cog = self # Instance of YourCog class
       bot.help_command = help_command


def setup(bot):
    bot.add_cog(YourCog(bot))

How does it work?

  1. It instantiates the HelpCommand class. help_command = MyHelp()
  2. It assigns the instance of YourCog(self) into cog attribute

Paginated Help Command

Seems like there are a lot of request for this to be made, and my original plan was to wait for menus to be integrated with buttons. However, looking at the current event of where it seems like that won't happen. To clarify, I will not be walking through all of the specific feature that the classes I'm using in this tutorial.

In this part, we will be using discord-ext-menus to handle our pagination. This is an external library that Danny wrote that seems to never reached 2.0. You might be asking, "why even bother using it if it won't ever reach 2.0". I will answer that by saying it is still useful for pagination use. You can even use this for other pagination, not just for help command. Even with 2.0, you will be able to use this efficiently with buttons. I will show that later in the walkthrough.

Another walkthrough I made regarding paginator, here I talk about it more in depth. click here

Pagination for 1.7.x

Now for the people who have never use ext-menus before, you may be asking me, "Stella, what the hell is a MenuPages class". To explain, this class inherits Menu class which handles everything such as waiting for user reactions, and adding reactions to your message. You can do fun things with Menu class such as a confirmation message with reactions which are shown here. But for our use, we will use them as the button for the pagination. For MenuPages, this class is specifically for handling pages, when you click on a reaction, it will show you the next page.

Now, here are the basic implementation for our use.

Code Example For MenuPages

import discord
from discord.ext import menus

class MyMenuPages(menus.MenuPages, inherit_buttons=False):
    @menus.button("<:before_check:754948796487565332>", position=menus.First(1))
    async def go_before(self, payload):
        """Goes to the previous page."""
        await self.show_checked_page(self.current_page - 1)

    @menus.button("<:next_check:754948796361736213>", position=menus.Last(0))
    async def go_after(self, payload):
        """Goes to the next page."""
        await self.show_checked_page(self.current_page + 1)

    @menus.button("<:before_fast_check:754948796139569224>", position=menus.First(0))
    async def go_first(self, payload):
        """Goes to the first page."""
        await self.show_page(0)

    @menus.button("<:next_fast_check:754948796391227442>", position=menus.Last(1))
    async def go_last(self, payload):
        """Goes to the last page."""
        await self.show_page(self._source.get_max_pages() - 1)
    
    @menus.button("<:stop_check:754948796365930517>", position=menus.First(2))
    async def go_stop(self, payload):
        """Remove this message."""
        self.stop()

Explanation

  1. inherit_buttons kwargs is set to False, this is to remove all default menus.button to set it your own.
  2. MenuPages.show_page is to show the page at a position that you gave, in this case, 0.
  3. MenuPages.show_checked_page is to show a page, similar to show_page, but this will check if the position exist. IndexError error are ignored when it is raised.
  4. First/Last class is an anchor during adding reaction. For example, when given as First(0), Last(0), First(1) First(2), Last(1). Regardless of order, First(0) reaction will be added first, followed by First(1), First(2). After that, Last(0) and Last(1) are added. There are also Position, but I don't use them here.
  5. MenuPages.stop() will end the menu session, you can set them to delete the message by delete_message_after or clear_reactions_after to clear the reactions after the menu session ended.

You will need to create ListPageSource class to use with this class. Jump Here

Pagination for 2.x

With the introduction of Buttons in 2.0. We are able to use them as a replacement of MenuPages. In fact, we can even re-use the code in MenuPages and recreate them for pagination.

Similar to MenuPages, we have View that handles all the interaction with the user. This can be use in Button, Select. It is documented here. There are example usage for View written by Danny himself. This will help you understand how to use them, as I won't be explaining every niche things about them. The example is found here where he wrote a tictactoe game with View and Button.

In order to create a Pagination view, we will also subclass the MenuPages class to borrow the features that are available from them. They handle the pages for us, which is insanely useful thanks to Danny's way of coding.

Example Code for View Button

import discord
from discord import ui
from discord.ext import menus

class MyMenuPages(ui.View, menus.MenuPages):
    def __init__(self, source, *, delete_message_after=False):
        super().__init__(timeout=60)
        self._source = source
        self.current_page = 0
        self.ctx = None
        self.message = None
        self.delete_message_after = delete_message_after

    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

    @ui.button(emoji='<:before_fast_check:754948796139569224>', style=discord.ButtonStyle.blurple)
    async def first_page(self, interaction, button):
        await self.show_page(0)
        await interaction.response.defer()

    @ui.button(emoji='<:before_check:754948796487565332>', style=discord.ButtonStyle.blurple)
    async def before_page(self, interaction, button):
        await self.show_checked_page(self.current_page - 1)
        await interaction.response.defer()

    @ui.button(emoji='<:stop_check:754948796365930517>', style=discord.ButtonStyle.blurple)
    async def stop_page(self, interaction, button):
        await interaction.response.defer()
        self.stop()
        if self.delete_message_after:
            await self.message.delete(delay=0)

    @ui.button(emoji='<:next_check:754948796361736213>', style=discord.ButtonStyle.blurple)
    async def next_page(self, interaction, button):
        await self.show_checked_page(self.current_page + 1)
        await interaction.response.defer()

    @ui.button(emoji='<:next_fast_check:754948796391227442>', style=discord.ButtonStyle.blurple)
    async def last_page(self, interaction, button):
        await self.show_page(self._source.get_max_pages() - 1)
        await interaction.response.defer()

Explanation

  1. ui.View contains all the handling of Button interaction. While, we also need partial use of menus.MenuPages where it handles the core of pagination. This include calling ListPageSource.format_page which will be use.
  2. We won't call super().start(...) because those handles reactions, we're only borrowing the methods from them.
  3. timeout=60 kwargs is passed to ui.View, Not menus.MenuPages. To understand how this works, learn Method Resolution Order (MRO). As I won't be covering them here.
  4. View.interaction_check is for the check. You can check if the interaction is your author. Returns True will result in calling the Button callback. Raising an error or returning False will prevent them from being called.
  5. ui.button basically is the button you're adding to View.
  6. for show_check_page/show_page are explained in the 1.7.x section. Jump Here
  7. message.delete(delay=0) is a way to delete message without checking if they exist. When error, it silences them.

You will need to create ListPageSource class to use with this class. Jump Here

ListPageSource class

As I stated before, ListPageSource are only from the external library discord-ext-menus. Make sure to install them.

Now MyMenuPages are created for our use, lets create our ListPageSource now. To put it simply, ListPageSource is to format each page where MyMenuPages will call this class. There are also other types of class format. However, I will just use ListPageSource for simplicity sake.

If you want to read more about ListPageSource, you can read them here. Click Here.

Now this will be our basic implementation of ListPageSource.

Example Code For ListPageSource

import discord
from itertools import starmap
from discord.ext import menus
class HelpPageSource(menus.ListPageSource):
    def __init__(self, data, helpcommand):
        super().__init__(data, per_page=6)
        self.helpcommand = helpcommand

    def format_command_help(self, no, command):
        signature = self.helpcommand.get_command_signature(command)
        docs = self.helpcommand.get_command_brief(command)
        return f"{no}. {signature}\n{docs}"
    
    async def format_page(self, menu, entries):
        page = menu.current_page
        max_page = self.get_max_pages()
        starting_number = page * self.per_page + 1
        iterator = starmap(self.format_command_help, enumerate(entries, start=starting_number))
        page_content = "\n".join(iterator)
        embed = discord.Embed(
            title=f"Help Command[{page + 1}/{max_page}]", 
            description=page_content,
            color=0xffcccb
        )
        author = menu.ctx.author
        embed.set_footer(text=f"Requested by {author}", icon_url=author.avatar_url)  # author.avatar in 2.0
        return embed

Explanation

  1. per_page kwargs is the amount of elements from data that will be passed to ListPageSource.format_page as entries.
  2. starting_number is just the starting number for the list of 6 that will be shown. So for example, at page 1, it will be 1, at page 2, it will be 13 and page 3 would be 19. menu.current_page will start at 0, hence we add 1.`
  3. itertools.starmap works exactly like map but it unpacks the argument when calling a function, in this case, HelpPageSource.format_command_help.
  4. We can get Context from menu.ctx attribute.
  5. For ListPageSource.format_page, whatever you return from this method, will be shown as a single page. You can return a discord.Embed, a str or a dict that will be as kwargs for message.edit.

The output of them are in the Integrating class of pagination section.

Integrating classes of pagination

Now we will be creating the HelpCommand that uses classes that we've made. This HelpCommand class will also be able to use the 2.0 button. Hence, why it's in its own section.

Example Code

import discord
from itertools import chain
from discord.ext import commands

class MyHelp(commands.MinimalHelpCommand):
    def get_command_brief(self, command):
        return command.short_doc or "Command is not documented."
    
    async def send_bot_help(self, mapping):
        all_commands = list(chain.from_iterable(mapping.values()))
        formatter = HelpPageSource(all_commands, self)
        menu = MyMenuPages(formatter, delete_message_after=True)
        await menu.start(self.context)

bot = commands.Bot("uwu ", help_command=MyHelp())

Explanation

  1. itertools.chain.from_iterable flatten the result of mapping.values() into a single list. This is up to you on how you want to handle the command. I'm just making it flat to be as basic as possible.
  2. HelpPageSource acts as the formatter, this class will be the one that gets the data and the one that shows each page to the user.
  3. MyMenuPages depending on which class you're using, this will handle the HelpPageSource class. It will handle the buttons from the user, and determine whether to show the previous/next/forward/backward or stop.
  4. Command.short_doc will show the value from Command.brief or if None, show the first sentence of Command.help.

The Output for 1.7.x

helpMenu

The Output for 2.x

helpMenu

The end

I hope that reading this walkthrough will assist you and give a better understanding on how to subclass HelpCommand. All the example code given are to demonstrate the feature of HelpCommand and feel free to try it. There are lots of creative things you can do to create a HelpCommand.

If you need an inspiration, this is my help command. I mean, nobody asked but yeah here it is. helpEx

Here's my code if you want to take a look. It's a bit complicated, but you only have to focus on the HelpCommand class my help command. If you want a more easier help command, here's an example of a help command written by pikaninja Here's the code Here's how it looks like.

help_simple

Now, of course, any question regarding HelpCommand should be asked in the discord.py server because I don't really check this gist as much, and because there is a lot of helpful discord.py helpers if you're nice enough to them. :D

If you're still confused about paginator, I've made a full walkthrough regarding menus and View pagination. Click Here

@pikaninja
Copy link

you do it the same way as outside, when defining your discord.Embed set the colour kwarg

@laughingwater
Copy link

you do it the same way as outside, when defining your discord.Embed set the colour kwarg

Yeah I figured that out myself lmao I was being dumb, but thanks anyways!

@InterStella0
Copy link
Author

InterStella0 commented Jun 27, 2021

How can I make the arguments non-case sensitive?

The problem was solved above, just scroll up you can see my answer on tackling it.

@Nirlep5252
Copy link

genius

@pikaninja
Copy link

for all the people asking questions here read the end >:(
image

@chirag-droid
Copy link

chirag-droid commented Jul 18, 2021

Thanks Sir, really helpful guide.
And I came to know about this when I wrote my own help command from scratch, handling everything myself.

@spl1ce0
Copy link

spl1ce0 commented Aug 4, 2021

hey, how can i make a paginated help command?

@ChesterChowWOV
Copy link

Well

IT IS THE BEST HELP COMMAND TUTORIAL EVER!!!
I love this!
Thank you very much for making this tutorial! I made a pretty help command!

@doumeem
Copy link

doumeem commented Aug 15, 2021

Best Walkthrough Ever.

@MatchTheMatrix
Copy link

MatchTheMatrix commented Aug 27, 2021

hey, how can i make a paginated help command?

Did you find it out? I want to make a help command with selects of Discord-Components :/

@InterStella0
Copy link
Author

InterStella0 commented Sep 4, 2021

Pagination is added to the walkthrough as a result of you guys keeps bothering me about it. They are covered here.

@Nirlep5252
Copy link

👀

@eitozx
Copy link

eitozx commented Oct 4, 2021

Awesome work, Thanks for sharing.

@bsod2528
Copy link

bsod2528 commented Oct 7, 2021

thx u guys uwu

your welcome sir

@MarzaElise
Copy link

hey

@InterStella0
Copy link
Author

Seems like people are still asking me about paginator stuff which is really not about this walkthrough. If you want to learn more about paginator, you can refer to here.

It's a walkthrough i wrote about pagination using discord-ext-menus and View similar to this walkthrough except more in depth.

@ptekelly
Copy link

ptekelly commented Apr 18, 2022

Hello Stella
I love this walkthrough - not only helped me create neater help command but teaching me a lot on classes.

@sgtlaggy
Copy link

sgtlaggy commented Jul 3, 2022

A lot of the 2.0 links are broken, /master/ was removed from docs, just /latest/ exists now.

@InterStella0
Copy link
Author

Hyperlinks are updated now, I was waiting for v2 to be released but it seems kinda long to wait for it.

@spl1ce0
Copy link

spl1ce0 commented Aug 24, 2022

Image links seem to not be working for me :( am I the only one?

@jvrring
Copy link

jvrring commented Aug 26, 2022

Image links seem to not be working for me :( am I the only one?

Nope, your not.
Clicking on them gives a "Not Found" error.

@InterStella0
Copy link
Author

I've readded the images because I accidentally deleted them during purging on files.

@tookender
Copy link

maybe you should also explain why having a slash help command is useless

@InterStella0
Copy link
Author

That seems self-explanatory considering that Discord itself lists the commands for you.

@Motzumoto
Copy link

We miss you in the dpy server :(

@InterStella0
Copy link
Author

Appreciate the kind gesture but i've nuked all of my socials for the sake of my mental health. also this is literally out of topic of the gist

@Motzumoto
Copy link

Appreciate the kind gesture but i've nuked all of my socials for the sake of my mental health. also this is literally out of topic of the gist

Im glad to hear you're still around, and yeah it is. Be safe <3

@brutiv
Copy link

brutiv commented Aug 31, 2024

Can anyone tell me how to make this a hybrid command?

@InterStella0
Copy link
Author

@Shefki-Berisha It is as simple as defining a slash command and reusing Context.send_help

@bot.tree.command()
async def help(interaction: Interaction, command_or_group: Optional[str]):
    ctx = await bot.get_context(interaction)
    entity = command_or_group and (command_or_group,) or ()
    await ctx.send_help(*entity)

Note that this wont include slash commands into your help output. That would be a bit more annoying to implement, so I have made a lib that implements it in starlight-dpy

@brutiv
Copy link

brutiv commented Sep 6, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment