2026, Jan 03 11:00

How to Build a 3x3 Discord Button Grid in discord.py Without ActionRow (Avoid the TypeError)

Solve the discord.py ActionRow TypeError when making Discord buttons. Learn to build a 3x3 tic-tac-toe grid using View and Button row for clean, modern UI.

Building interactive games with Discord buttons looks straightforward until you hit a wall with ActionRow. Many snippets floating around still use ActionRow directly, but trying to follow them in modern discord.py leads to a cryptic TypeError. Let’s unpack what’s going on and how to structure a 3×3 board of buttons the correct way.

Problem overview

Consider a function that assembles a tic-tac-toe–style grid. The intent is simple: create buttons, group them into rows, and add those rows to a View. The call to ActionRow causes the error.

async def build_board(self, ui_view, on_press):
    tiles = []
    for r in range(3):
        row_items = []
        for c in range(3):
            btn = discord.ui.Button(label="\u200b", style=discord.ButtonStyle.gray, custom_id=f"{r}_{c}")
            btn.callback = on_press
            row_items.append(btn)
        tiles.append(row_items)
    print(len(tiles[0]))
    ui_view.add_item(discord.ActionRow(*tiles[0]))
    ui_view.add_item(discord.ActionRow(*tiles[1]))
    ui_view.add_item(discord.ActionRow(*tiles[2]))
    return tiles

This leads to the following runtime error:

TypeError: ActionRow.__init__() takes 2 positional arguments but 4 were given

What’s actually wrong

The failure isn’t in button creation; it’s in how ActionRow is being used. In current libraries, ActionRow isn’t meant to be constructed this way in user code. Old tutorials suggest instantiating it directly and passing multiple buttons, but that no longer aligns with how the UI system expects you to build views.

The short version: you don’t need to touch ActionRow. The View handles rows internally, and the row each button belongs to is controlled right on the Button itself.

Put buttons straight into the View and assign their row with Button(..., row=number).

The fix

Create Button instances with the row parameter and add them directly to the View. No explicit ActionRow construction is required.

class GridView(discord.ui.View):
    def __init__(self):
        super().__init__()
        for y in range(3):
            for x in range(3):
                element = discord.ui.Button(
                    label=f"Button {y+1}.{x+1}",
                    style=discord.ButtonStyle.gray,
                    custom_id=f"{y}_{x}",
                    row=y
                )
                self.add_item(element)

To display it, create an instance of the view and send it with your message.

@bot.command()
async def start(ctx):
    game_view = GridView()
    await ctx.send(view=game_view)

This approach has been tested with discord.py 2.5.1, py-cord 2.6.1, and nextcord 3.1.0.

Why this matters

UI APIs evolve. Tutorials that suggest manual ActionRow construction are out of date in this part of the API surface. Relying on them leads to confusing errors that have nothing to do with your game logic. Using Button(..., row=...) keeps your code aligned with how the View system arranges components today and avoids low-level internals.

Takeaways

If you see ActionRow in older examples, don’t copy it into new code. Add buttons directly to the View and use the row parameter to position them. This keeps your components predictable, your code smaller, and your UI consistent across supported libraries.