Embeds in discord.py

An embed is the boxed message with a coloured bar down the side: a title, text, fields in columns, images and a footer. Here is every part of one, how to send and edit them, and what Discord rejects.

8 min readUpdated

This is for discord.py 2 (pip show discord.py to check). The examples are slash commands; with prefix commands the embed is the same object, sent with await ctx.send(embed=embed).

The smallest embed

Python
@bot.tree.command(description="Send an embed")
async def info(interaction: discord.Interaction):
    embed = discord.Embed(
        title="Hello",
        description="This is an embed.",
        colour=discord.Colour.blurple(),
    )
    await interaction.response.send_message(embed=embed)

Every part, in one example

Python
embed = discord.Embed(
    title="Server report",
    url="https://example.com/report",       # makes the title a link
    description="Everything from the last **24 hours**.",
    colour=discord.Colour.from_str("#2ecc71"),
    timestamp=discord.utils.utcnow(),       # shown in each reader's own time zone
)
embed.set_author(name=interaction.user.display_name, icon_url=interaction.user.display_avatar.url)
embed.set_thumbnail(url="https://example.com/logo.png")   # small, top right
embed.add_field(name="New members", value="12")            # inline: side by side
embed.add_field(name="Messages", value="3,406")
embed.add_field(name="Busiest channel", value="#general", inline=False)  # a row of its own
embed.set_image(url="https://example.com/chart.png")       # large, at the bottom
embed.set_footer(text="Updated every hour")
  • Top to bottom it reads: author, title, description, fields, image, then the footer and timestamp on one line. The thumbnail sits at the top right.
  • Inline fields go up to three to a row on a wide screen and stack on a phone. inline=False starts a new row.
  • Markdown works in the description and field values: bold, lists, [masked links](https://example.com).
  • Mentions such as <@user_id> or <#channel_id> show as mentions inside an embed but never notify anyone. If someone should get pinged, put the mention in the message content as well.
  • Every set_ and add_ method returns the embed, so they can be chained: discord.Embed(title="Hi").add_field(name="a", value="b").

Colours

Python
discord.Colour.blurple()              # Discord's own blue
discord.Colour.red()                  # also green(), gold(), teal(), dark_grey() and more
discord.Colour.from_str("#5865F2")
discord.Colour.from_rgb(88, 101, 242)
0x5865F2                              # a plain number works too

colour= and color= are the same argument; use whichever you spell.

Images from your own files

An image does not have to be online already. Upload it with the message and point the embed at it with attachment://:

Python
file = discord.File("charts/today.png", filename="chart.png")
embed = discord.Embed(title="Today")
embed.set_image(url="attachment://chart.png")
await interaction.response.send_message(embed=embed, file=file)

The name after attachment:// has to be exactly the filename. Keep it to letters, numbers and dashes: Discord changes some characters in uploaded file names, and then the two no longer match and the picture shows up as a separate attachment instead of inside the embed. An image made in memory (a chart from matplotlib, say) works the same way with discord.File(io.BytesIO(png_bytes), filename="chart.png").

Timestamps

timestamp=discord.utils.utcnow() puts the time in the footer, and every reader sees it in their own time zone. Avoid datetime.datetime.utcnow(): it has no time zone attached, discord.py then takes it as the machine's local time, and the embed is off by however far that machine is from UTC. It can look right on a server set to UTC and be hours out on your laptop, or the other way round.

For a time inside the text, discord.utils.format_dt(when, "R") gives Discord's own timestamp format, shown as "in 2 hours" and counting down live.

Several embeds in one message

Python
await interaction.response.send_message(embeds=[first, second, third])

Up to 10. Pass embed= or embeds=, not both: discord.py stops with TypeError: Cannot mix embed and embeds keyword arguments.

Editing an embed

Change the object and send it again. After replying to a slash command, edit_original_response edits that reply:

Python
import asyncio

@bot.tree.command(description="Count down from five")
async def countdown(interaction: discord.Interaction):
    embed = discord.Embed(title="Countdown", description="5")
    await interaction.response.send_message(embed=embed)
    for n in range(4, -1, -1):
        await asyncio.sleep(1)
        embed.description = str(n) if n else "Lift off"
        await interaction.edit_original_response(embed=embed)

For an ordinary message, keep what send returns and call await message.edit(embed=embed). To change an embed that is already in a channel, start from a copy of it: embed = message.embeds[0].copy(). set_field_at(0, name=..., value=...) replaces one field, remove_field(0) drops one, and clear_fields() empties them.

Embeds from JSON

Embed.from_dict takes the same JSON Discord itself uses, which is handy for messages kept in a config file or made in an online embed builder:

Python
embed = discord.Embed.from_dict({
    "title": "Rules",
    "description": "Be kind. No spam.",
    "color": 0x5865F2,
    "fields": [{"name": "Appeals", "value": "Message a moderator", "inline": False}],
})

embed.to_dict() goes the other way, to save one.

The limits

PartLimit
Title256 characters
Description4,096 characters
Fields25 per embed
Field name256 characters
Field value1,024 characters
Footer text2,048 characters
Author name256 characters
All of the above, across every embed in the message6,000 characters
Embeds per message10

len(embed) counts exactly the characters that go towards the 6,000, so you can check before sending. Going over any limit gets a 400 Bad Request (error code: 50035): Invalid Form Body with a second line naming the part, such as In embeds.0.title: Must be 256 or fewer in length. Long lists are better split over several embeds or pages than trimmed until they fit.

When an embed does not show up

  • The bot is missing Embed Links in that channel. Without it, the embed does not appear. Check the channel's own permission overrides as well as the bot's role.
  • In embeds.0.fields.0.value: This field is required: a field name or value is an empty string, usually a list joined with nothing in it. Give it a fallback: value=", ".join(names) or "Nobody yet". (None does not fail; it shows the word None, which is its own surprise.)
  • The image is missing: the URL has to be the image itself over http or https, not a web page that shows it. For uploaded files, the attachment:// name must match the filename.
  • The time is hours out: a naive datetime. Use discord.utils.utcnow(), as above.
  • The command says The application did not respond: building the embed took more than three seconds (an API call, a database). await interaction.response.defer() first, then send it with interaction.followup.send(embed=embed). More in slash commands in discord.py.

New to discord.py? Make a Discord bot in Python starts from nothing. When the bot is ready to stay online, SnowServers runs discord.py bots from $3 a month with a 7-day free trial.

Something here wrong or out of date? Tell us and it gets fixed.