Skip to main content
Pycord v2.0 introduced new Discord features and made several breaking changes to support modern Discord functionality. This guide will help you migrate your bot from v1.x to v2.x.

Python Version Requirements

Breaking Change: Python 3.7 and below are no longer supported.
Pycord v2.0 requires Python 3.8 or higher. Make sure to upgrade your Python version before migrating.

Major Changes

Dropped User Account Support

User account support has been removed as it violates Discord’s Terms of Service. The following features have been removed:
All user account-related features have been completely removed:
  • bot argument of Client.start() and Client.run()
  • afk argument of Client.change_presence()
  • Classes: Profile, Relationship, CallMessage, GroupCall
  • Client.self_bot, Client.fetch_user_profile()
  • ClientUser.email, premium, premium_type, relationships, friends, blocked
  • Events: on_relationship_add, on_relationship_update

Timezone-Aware Datetime Objects

All datetime objects are now timezone-aware and use UTC.
from datetime import datetime

embed = discord.Embed(
    title="Pi Day 2021",
    timestamp=datetime(2021, 3, 14, 15, 9, 2)
)

Asset Changes

Asset-related attributes now return Asset objects instead of hash strings.
# Getting avatar URL
avatar_url = str(user.avatar_url)

# Custom size
avatar_url = str(user.avatar_url_as(size=128))

# Static format
avatar_url = str(user.avatar_url_as(size=128, static_format="png"))

# Reading avatar data
data = await user.avatar_url.read()
Important: User.avatar now returns None if the user has no custom avatar. Use User.display_avatar to get the avatar or default avatar.

Emoji URL Changes

emoji_url = str(emoji.url)
resized_url = str(emoji.url_as(size=32))

Webhook Changes

Webhooks are now split into async and sync variants.
# Async webhook
webhook = discord.Webhook.from_url(
    webhook_url,
    adapter=discord.AsyncWebhookAdapter(session)
)
await webhook.send("Hello!")
WebhookAdapter, AsyncWebhookAdapter, and RequestsWebhookAdapter have been removed.

Intents Changes

Message Content Intent

Breaking Change: Intents.message_content is now a privileged intent.
Without this intent enabled:
  • Message.content will be an empty string
  • Message.embeds will be an empty list
  • Message.attachments will be an empty list
  • Message.components will be an empty list
  • Text commands will not work
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
# Message content was available by default
You must also enable this intent in the Discord Developer Portal.

Threads Support

Threads are now supported as first-class citizens. The following methods can now return Thread objects:
  • Message.channel
  • Client.fetch_channel()
  • Guild.fetch_channel()
  • Client.get_channel()
# Check if channel is a thread
if isinstance(channel, discord.Thread):
    print(f"Thread: {channel.name}")
    print(f"Parent channel: {channel.parent}")

Permission Changes

# Check permissions
perms = user.permissions_in(channel)
if perms.administrator:
    print("User is admin")

Edit Method Behavior

Breaking Change: edit() methods no longer update objects in-place.
await message.edit(content="Updated content")
# message object is automatically updated
print(message.content)  # "Updated content"

Positional and Keyword Arguments

Now Positional-Only

The following methods now require positional arguments:
# These must be called with positional args
channel.permissions_for(member)  # Not permissions_for(member=member)
guild.get_channel(channel_id)    # Not get_channel(id=channel_id)
guild.get_role(role_id)
guild.fetch_member(member_id)
messageable.fetch_message(message_id)

Now Keyword-Only

The following methods now require keyword arguments:
# These must be called with keyword args
url = discord.utils.oauth_url(client_id, permissions=permissions)
users = reaction.users(limit=100)

Event Changes

Presence Updates

@bot.event
async def on_member_update(before, after):
    # Handled both attribute and presence updates
    if before.status != after.status:
        print(f"{after.name} is now {after.status}")

Removed Events

The following events have been removed:
  • on_private_channel_create
  • on_private_channel_delete
  • on_relationship_add
  • on_relationship_update

Message Type for Replies

if message.type == discord.MessageType.default:
    # This matched both regular messages and replies
    print("Regular message")

Sticker Changes

  • Sticker.preview_image was removed
  • StickerType was renamed to StickerFormatType
  • Message.stickers now returns List[StickerItem] instead of List[Sticker]
  • Use StickerItem.fetch() to get the full Sticker object
# Fetch full sticker information
for sticker_item in message.stickers:
    sticker = await sticker_item.fetch()
    print(f"Sticker: {sticker.name}")

Type Changes

Optional Attributes

Many attributes can now be None:
  • DMChannel.recipient can be None
  • User.avatar can be None (use User.display_avatar instead)
  • Guild.vanity_invite can be None
  • Context attributes: prefix, command, invoked_with, invoked_subcommand can be None
# Always check for None
if user.avatar is not None:
    print(user.avatar.url)
else:
    print(user.default_avatar.url)

# Or use display_avatar
print(user.display_avatar.url)  # Always works

Miscellaneous Changes

Removed Features

The following have been removed:
  • Client.request_offline_members()
  • Client.logout() (use Client.close() instead)
  • ExtensionNotFound.original
  • MemberCacheFlags.online
  • guild_subscriptions argument
  • fetch_offline_members argument
  • VerificationLevel.table_flip and related aliases

Renamed Features

# Blurple color
color = discord.Colour.blurple()

# Missing permissions
try:
    await command()
except discord.ext.commands.MissingPermissions as e:
    print(e.missing_perms)

Behavior Changes

  • Embed objects with any content are now always truthy
  • Bot.add_cog() now raises an error if a cog with the same name exists (use override=True to bypass)
  • StageChannel.edit() can no longer edit topic (use StageInstance.edit() instead)
  • Reaction.custom_emoji is now Reaction.is_custom_emoji()
# Adding cogs safely
try:
    bot.add_cog(MyCog())
except discord.ClientException:
    # Cog already exists
    bot.add_cog(MyCog(), override=True)

New Features in v2.0

While migrating, take advantage of these new v2.0 features:
  • Slash Commands: Modern interaction-based commands
  • UI Components: Buttons, select menus, modals
  • Application Commands: Slash commands, user commands, message commands
  • Threads: Full thread support
  • Stage Channels: Complete stage channel functionality
For more information on these features, check out the Pycord Guide.

Migration Checklist

  • Update Python to 3.8 or higher
  • Update all datetime objects to be timezone-aware
  • Replace user.avatar_url with user.display_avatar.url
  • Update webhook implementations
  • Enable message content intent if using text commands
  • Update from user.permissions_in() to channel.permissions_for()
  • Capture return values from edit() methods
  • Split on_member_update into on_member_update and on_presence_update
  • Update message type checks for replies
  • Replace Client.logout() with Client.close()
  • Update missing_perms to missing_permissions
  • Add thread type checking where needed
  • Test all functionality thoroughly

Getting Help

If you need help migrating, join the Pycord Discord Server where the community can assist you.

Build docs developers (and LLMs) love