Migrating to v2.0

Compared to v1.0, v2.0 mostly has breaking changes related to better developer experience and API coverage. While the changes aren’t as massive to require an entire rewrite, there are still many changes that need to be accounted for.

Python Version Change

In order to ease development, maintain security updates, and use newer features v2.0 drops support for Python 3.7 and earlier.

Removal of Support For User Accounts

Logging on with a user token is against the Discord Terms of Service and as such all support for user-only endpoints has been removed.

The following have been removed:

  • bot parameter to Client.login() and Client.start()

  • afk parameter to Client.change_presence()

  • password, new_password, email, and house parameters to ClientUser.edit()

  • CallMessage model

  • GroupCall model

  • Profile model

  • Relationship model

  • RelationshipType enumeration

  • HypeSquadHouse enumeration

  • PremiumType enumeration

  • UserContentFilter enumeration

  • FriendFlags enumeration

  • Theme enumeration

  • on_relationship_add event

  • on_relationship_remove event

  • on_relationship_update event

  • Client.fetch_user_profile method

  • ClientUser.create_group method

  • ClientUser.edit_settings method

  • ClientUser.get_relationship method

  • GroupChannel.add_recipients method

  • GroupChannel.remove_recipients method

  • GroupChannel.edit method

  • Guild.ack method

  • Message.ack method

  • User.block method

  • User.is_blocked method

  • User.is_friend method

  • User.profile method

  • User.remove_friend method

  • User.send_friend_request method

  • User.unblock method

  • ClientUser.blocked attribute

  • ClientUser.email attribute

  • ClientUser.friends attribute

  • ClientUser.premium attribute

  • ClientUser.premium_type attribute

  • ClientUser.relationships attribute

  • Message.call attribute

  • User.mutual_friends attribute

  • User.relationship attribute

asyncio Event Loop Changes

Python 3.7 introduced a new helper function asyncio.run() which automatically creates and destroys the asynchronous event loop.

In order to support this, the way discord.py handles the asyncio event loop has changed.

This allows you to rather than using Client.run() create your own asynchronous loop to setup other asynchronous code as needed.

Quick example:

client = discord.Client()

async def main():
    # do other async things
    await my_async_function()

    # start the client
    async with client:
        await client.start(TOKEN)

asyncio.run(main())

A new setup_hook() method has also been added to the Client class. This method is called after login but before connecting to the discord gateway.

It is intended to be used to setup various bot features in an asynchronous context.

setup_hook() can be defined by subclassing the Client class.

Quick example:

class MyClient(discord.Client):
    async def setup_hook(self):
        print('This is asynchronous!')

client = MyClient()
client.run(TOKEN)

With this change, constructor of Client no longer accepts connector and loop parameters.

In parallel with this change, changes were made to loading and unloading of commands extension extensions and cogs, see Extension and Cog Loading / Unloading is Now Asynchronous for more information.

Intents Are Now Required

In earlier versions, the intents keyword argument was optional and defaulted to Intents.default(). In order to better educate users on their intents and to also make it more explicit, this parameter is now required to pass in.

For example:

# before
client = discord.Client()

# after
intents = discord.Intents.default()
client = discord.Client(intents=intents)

This change applies to all subclasses of Client.

Abstract Base Classes Changes

Abstract Base Classes that inherited from abc.ABCMeta now inherit from typing.Protocol.

This results in a change of the base metaclass used by these classes but this should generally be completely transparent to the user.

All of the classes are either runtime-checkable protocols or explicitly inherited from and as such usage with isinstance() and issubclass() is not affected.

The following have been changed to runtime-checkable Protocols:

The following have been changed to subclass Protocol:

The following have been changed to use the default metaclass instead of abc.ABCMeta:

datetime Objects Are Now UTC-Aware

All usage of naive datetime.datetime objects in the library has been replaced with aware objects using UTC timezone. Methods that accepted naive datetime objects now also accept timezone-aware objects. To keep behavior inline with datetime’s methods, this library’s methods now assume that naive datetime objects are local time (note that some of the methods may not accept naive datetime, such exceptions are listed below).

Because naive datetime objects are treated by many of its methods as local times, the previous behavior was more likely to result in programming errors with their usage.

To ease the migration, utils.utcnow() helper function has been added.

Warning

Using datetime.datetime.utcnow() can be problematic since it returns a naive UTC datetime object.

Quick example:

# before
week_ago = datetime.datetime.utcnow() - datetime.timedelta(days=7)
if member.created_at > week_ago:
    print(f'Member account {member} was created less than a week ago!')

# after
# The new helper function can be used here:
week_ago = discord.utils.utcnow() - datetime.timedelta(days=7)
# ...or the equivalent result can be achieved with datetime.datetime.now():
week_ago = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=7)
if member.created_at > week_ago:
    print(f'Member account {member} was created less than a week ago!')

The following have been changed from naive to aware datetime objects in UTC:

The following now accept aware datetime and assume that if the passed datetime is naive, it is a local time:

Currently, there’s only one place in this library that doesn’t accept naive datetime.datetime objects:

  • timed_out_until parameter in Member.edit()

    This has been done to prevent users from mistakenly applying incorrect timeouts to guild members.

Major Webhook Changes

Webhook support has been rewritten to work better with typings and rate limits.

As a result, synchronous functionality has been split to separate classes.

Quick example for asynchronous webhooks:

# before
async with aiohttp.ClientSession() as session:
    webhook = discord.Webhook.from_url('url-here', adapter=discord.AsyncWebhookAdapter(session))
    await webhook.send('Hello World', username='Foo')

# after
async with aiohttp.ClientSession() as session:
    webhook = discord.Webhook.from_url('url-here', session=session)
    await webhook.send('Hello World', username='Foo')

Quick example for synchronous webhooks:

# before
webhook = discord.Webhook.partial(123456, 'token-here', adapter=discord.RequestsWebhookAdapter())
webhook.send('Hello World', username='Foo')

# after
webhook = discord.SyncWebhook.partial(123456, 'token-here')
webhook.send('Hello World', username='Foo')

The following breaking changes have been made:

Asset Redesign and Changes

The Asset object now encompasses all of the methods and attributes related to a CDN asset.

This means that all models with asset-related attribute and methods have been transformed to use this new design. As an example, here’s how these changes look for Guild.icon (of Asset type):

In addition to this, Emoji and PartialEmoji now also share an interface similar to Asset’s:

Asset now always represent an actually existing CDN asset. This means that:

  • str(x) on an Asset can no longer return an empty string.

  • bool(x) on an Asset can no longer return False.

  • Attributes containing an optional Asset can now be None.

The following were affected by this change:

Thread Support

v2.0 has been updated to use a newer API gateway version which supports threads and as a result of this had to make few breaking changes. Most notably messages sent in guilds can, in addition to a TextChannel, be sent in a Thread.

The main differences between text channels and threads are:

For convenience, Thread has a set of properties and methods that return the information about the parent channel:

  • Thread.category

  • Thread.category_id

  • Thread.is_news()

  • Thread.is_nsfw()

  • Thread.permissions_for()

    • Note that this outputs the permissions of the parent channel and you might need to check for different permissions when trying to determine if a member can do something.

      Here are some notable examples:

      • A guild member can send messages in a text channel if they have send_messages permission in it.

        A guild member can send messages in a public thread if:
        A guild member can send messages in a private thread if:
      • A guild member can edit a text channel if they have manage_channels permission in it.

        A guild member can edit a thread if they have manage_threads permission in its parent channel.

        Note

        A thread’s owner can archive a (not-locked) thread and edit its name and auto_archive_duration without manage_threads permission.

      • A guild member can react with an emoji to messages in a text channel if:
        A guild member can react with an emoji to messages in a public thread if:
        • They have read_message_history permission in its parent channel.

        • They have add_reactions permission in its parent channel or the message already has that emoji reaction.

        • The thread is not archived. Note that the guild member can unarchive a thread (if it’s not locked) to react to a message.

        A guild member can react with an emoji to messages in a private thread if:
        • They have read_message_history permission in its parent channel.

        • They have add_reactions permission in its parent channel or the message already has that emoji reaction.

        • They’re either already a member of the thread or have a manage_threads permission in its parent channel.

        • The thread is not archived. Note that the guild member can unarchive a thread (if it’s not locked) to react to a message.

The following changes have been made:

Removing In-Place Edits

Most of the model methods that previously edited the model in-place have been updated to no longer do this. Instead, these methods will now return a new instance of the newly updated model. This has been done to avoid the library running into race conditions between in-place edits and gateway events on model updates. See GH-4098 for more information.

Quick example:

# before
await member.edit(nick='new nick')
await member.send(f'Your new nick is {member.nick}')

# after
updated_member = await member.edit(nick='new nick')
await member.send(f'Your new nick is {updated_member.nick}')

The following have been changed:

Sticker Changes

Discord has changed how their stickers work and as such, sticker support has been reworked.

The following breaking changes have been made:

Integrations Changes

To support the new integration types, integration support has been reworked.

The following breaking changes have been made:

Presence Updates Now Have A Separate Event

Presence updates (changes in member’s status and activity) now have a separate on_presence_update() event. on_member_update() event is now only called on member updates (changes in nickname, role, pending status, etc.).

From API perspective, these are separate events and as such, this change improves library’s consistency with the API. Presence updates usually are 90% of all handled events so splitting these should benefit listeners that were only interested in member updates.

Quick example:

# before
@client.event
async def on_member_update(self, before, after):
    if before.nick != after.nick:
        await nick_changed(before, after)
    if before.status != after.status:
        await status_changed(before, after)

# after
@client.event
async def on_member_update(self, before, after):
    if before.nick != after.nick:
        await nick_changed(before, after)

@client.event
async def on_presence_update(self, before, after):
    if before.status != after.status:
        await status_changed(before, after)

Moving Away From Custom AsyncIterator

Asynchronous iterators in v1.0 were implemented using a special class named AsyncIterator. v2.0 instead provides regular asynchronous iterators with no added utility methods.

This means that usage of the following utility methods is no longer possible:

  • AsyncIterator.next()

    Usage of an explicit async for loop should generally be preferred:

    # before
    it = channel.history()
    while True:
        try:
            message = await self.next()
        except discord.NoMoreItems:
            break
        print(f'Found message with ID {message.id}')
    
    # after
    async for message in channel.history():
        print(f'Found message with ID {message.id}')
    

    If you need to get next item from an iterator without a loop, you can use anext() (new in Python 3.10) or __anext__() instead:

    # before
    it = channel.history()
    first = await it.next()
    if first.content == 'do not iterate':
        return
    async for message in it:
        ...
    
    # after
    it = channel.history()
    first = await anext(it)  # await it.__anext__() on Python<3.10
    if first.content == 'do not iterate':
        return
    async for message in it:
        ...
    
  • AsyncIterator.get()

    # before
    msg = await channel.history().get(author__name='Dave')
    
    # after
    msg = await discord.utils.get(channel.history(), author__name='Dave')
    
  • AsyncIterator.find()

    def predicate(event):
        return event.reason is not None
    
    # before
    event = await guild.audit_logs().find(predicate)
    
    # after
    event = await discord.utils.find(predicate, guild.audit_logs())
    
  • AsyncIterator.flatten()

    # before
    users = await reaction.users().flatten()
    
    # after
    users = [user async for user in reaction.users()]
    
  • AsyncIterator.chunk()

    # before
    async for leader, *users in reaction.users().chunk(3):
        ...
    
    # after
    async for leader, *users in discord.utils.as_chunks(reaction.users(), 3):
        ...
    
  • AsyncIterator.map()

    # before
    content_of_messages = []
    async for content in channel.history().map(lambda m: m.content):
        content_of_messages.append(content)
    
    # after
    content_of_messages = [message.content async for message in channel.history()]
    
  • AsyncIterator.filter()

    def predicate(message):
        return not message.author.bot
    
    # before
    user_messages = []
    async for message in channel.history().filter(lambda m: not m.author.bot):
        user_messages.append(message)
    
    # after
    user_messages = [message async for message in channel.history() if not m.author.bot]
    

To ease this transition, these changes have been made:

The return type of the following methods has been changed to an asynchronous iterator:

The NoMoreItems exception was removed as calling anext() or __anext__() on an asynchronous iterator will now raise StopAsyncIteration.

Changing certain lists to be lazy sequences instead

In order to improve performance when calculating the length of certain lists, certain attributes were changed to return a sequence rather than a list.

A sequence is similar to a list except it is read-only. In order to get a list again you can call list on the resulting sequence.

The following properties were changed to return a sequence instead of a list:

This change should be transparent, unless you are modifying the sequence by doing things such as list.append.

Embed Changes

Originally, embeds used a special sentinel to denote emptiness or remove an attribute from display. The Embed.Empty sentinel was made when Discord’s embed design was in a nebulous state of flux. Since then, the embed design has stabilised and thus the sentinel is seen as legacy.

Therefore, Embed.Empty has been removed in favour of None.

Additionally, Embed.__eq__ has been implemented thus embeds becoming unhashable (e.g. using them in sets or dict keys).

# before
embed = discord.Embed(title='foo')
embed.title = discord.Embed.Empty
embed == embed.copy() # False

# after
embed = discord.Embed(title='foo')
embed.title = None
embed == embed.copy() # True
{embed, embed} # Raises TypeError

Removal of InvalidArgument Exception

The custom InvalidArgument exception has been removed and functions and methods that raised it are now raising TypeError and/or ValueError instead.

The following methods have been changed:

Logging Changes

The library now provides a default logging configuration if using Client.run(). To disable it, pass None to the log_handler keyword parameter. Since the library now provides a default logging configuration, certain methods were changed to no longer print to sys.stderr but use the logger instead:

For more information, check Setting Up Logging.

Text in Voice

In order to support text in voice functionality, a few changes had to be made:

In the future this may include StageChannel when Discord implements it.

Removal of StoreChannel

Discord’s API has removed store channels as of March 10th, 2022. Therefore, the library has removed support for it as well.

This removes the following:

  • StoreChannel

  • commands.StoreChannelConverter

  • ChannelType.store

Change in Guild.bans endpoint

Due to a breaking API change by Discord, Guild.bans() no longer returns a list of every ban in the guild but instead is paginated using an asynchronous iterator.

# before

bans = await guild.bans()

# after
async for ban in guild.bans(limit=1000):
    ...

Flag classes now have a custom bool() implementation

To allow library users to easily check whether an instance of a flag class has any flags enabled, using bool on them will now only return True if at least one flag is enabled.

This means that evaluating instances of the following classes in a bool context (such as if obj:) may no longer return True:

Function Signature Changes

Parameters in the following methods are now all positional-only:

The following parameters are now positional-only:

The following are now keyword-only:

The library now less often uses None as the default value for function/method parameters.

As a result, these parameters can no longer be None:

Allowed types for the following parameters have been changed:

Attribute Type Changes

The following changes have been made:

Removals

The following deprecated functionality have been removed:

  • Client.request_offline_members

  • AutoShardedClient.request_offline_members

  • Client.logout

  • fetch_offline_members parameter from Client constructor

    • Use chunk_guild_at_startup instead.

  • Permissions.use_slash_commands and PermissionOverwrite.use_slash_commands

The following have been removed:

  • MemberCacheFlags.online

    • There is no replacement for this one. The current API version no longer provides enough data for this to be possible.

  • AppInfo.summary

    • There is no replacement for this one. The current API version no longer provides this field.

  • User.permissions_in and Member.permissions_in

  • guild_subscriptions parameter from Client constructor

    • The current API version no longer provides this functionality. Use intents parameter instead.

  • VerificationLevel aliases:

  • topic parameter from StageChannel.edit()

  • Reaction.custom_emoji

  • AuditLogDiff.region

  • Guild.region

  • VoiceRegion

    • This has been marked deprecated by Discord and it was usually more or less out of date due to the pace they added them anyway.

  • region parameter from Client.create_guild()

  • region parameter from Template.create_guild()

  • region parameter from Guild.edit()

  • on_private_channel_create event

    • Discord API no longer sends channel create event for DMs.

  • on_private_channel_delete event

    • Discord API no longer sends channel create event for DMs.

  • The undocumented private on_socket_response event

  • abc.Messageable.trigger_typing

Miscellaneous Changes

The following changes have been made:

VoiceProtocol.connect() signature changes.

VoiceProtocol.connect() will now be passed 2 keyword only arguments, self_deaf and self_mute. These indicate whether or not the client should join the voice chat being deafened or muted.

Command Extension Changes

Extension and Cog Loading / Unloading is Now Asynchronous

As an extension to the asyncio changes the loading and unloading of extensions and cogs is now asynchronous.

To accommodate this, the following changes have been made:

Quick example of an extension setup function:

# before
def setup(bot):
    bot.add_cog(MyCog(bot))

# after
async def setup(bot):
    await bot.add_cog(MyCog(bot))

Quick example of loading an extension:

# before
bot.load_extension('my_extension')

# after using setup_hook
class MyBot(commands.Bot):
    async def setup_hook(self):
        await self.load_extension('my_extension')

# after using async_with
async def main():
    async with bot:
        await bot.load_extension('my_extension')
        await bot.start(TOKEN)

asyncio.run(main())

Converters Are Now Generic Runtime Protocols

Converter is now a runtime-checkable typing.Protocol.

This results in a change of the base metaclass used by these classes which may affect user-created classes that inherit from Converter.

Quick example:

# before
class SomeConverterMeta(type):
    ...

class SomeConverter(commands.Converter, metaclass=SomeConverterMeta):
    ...

# after
class SomeConverterMeta(type(commands.Converter)):
    ...

class SomeConverter(commands.Converter, metaclass=SomeConverterMeta):
    ...

In addition, Converter is now a typing.Generic which (optionally) allows the users to define their type hints more accurately.

Function Signature Changes

Parameters in the following methods are now all positional-only:

The following parameters are now positional-only:

The following parameters have been removed:

The library now less often uses None as the default value for function/method parameters.

As a result, these parameters can no longer be None:

Removals

The following attributes have been removed:

Miscellaneous Changes

Tasks Extension Changes

Migrating to v1.0

The contents of that migration has been moved to Migrating to v1.0.