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
@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
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=Falsestarts 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_andadd_method returns the embed, so they can be chained:discord.Embed(title="Hi").add_field(name="a", value="b").
Colours
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 toocolour= 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://:
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
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:
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:
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
| Part | Limit |
|---|---|
| Title | 256 characters |
| Description | 4,096 characters |
| Fields | 25 per embed |
| Field name | 256 characters |
| Field value | 1,024 characters |
| Footer text | 2,048 characters |
| Author name | 256 characters |
| All of the above, across every embed in the message | 6,000 characters |
| Embeds per message | 10 |
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". (Nonedoes 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
httporhttps, not a web page that shows it. For uploaded files, theattachment://name must match thefilename. - The time is hours out: a naive
datetime. Usediscord.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 withinteraction.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.