From c73d4ce947937501dc75bd3fe6ffcae40d96f12f Mon Sep 17 00:00:00 2001 From: Paillat Date: Fri, 27 Mar 2026 23:52:10 +0100 Subject: [PATCH 1/4] fix: Update event loop retrieval to support Python 3.14+ --- discord/client.py | 12 +++++++++--- discord/ext/commands/cooldowns.py | 3 ++- discord/ext/tasks/__init__.py | 8 ++++---- discord/http.py | 4 ++-- discord/ui/modal.py | 4 ++-- discord/utils.py | 18 ++++++++++++++++++ 6 files changed, 37 insertions(+), 12 deletions(-) diff --git a/discord/client.py b/discord/client.py index 211532d68d..f227ccf1df 100644 --- a/discord/client.py +++ b/discord/client.py @@ -70,7 +70,13 @@ from .threads import Thread from .ui.view import BaseView from .user import ClientUser, User -from .utils import _D, _FETCHABLE, MISSING, warn_if_voice_dependencies_missing +from .utils import ( + _D, + _FETCHABLE, + MISSING, + _get_event_loop, + warn_if_voice_dependencies_missing, +) from .webhook import Webhook from .widget import Widget @@ -147,7 +153,7 @@ class Client: loop: Optional[:class:`asyncio.AbstractEventLoop`] The :class:`asyncio.AbstractEventLoop` to use for asynchronous operations. Defaults to ``None``, in which case the default event loop is used via - :func:`asyncio.get_event_loop()`. + :func:`asyncio.get_event_loop()` if it exists or one is created via :func:`asyncio.new_event_loop()`. connector: Optional[:class:`aiohttp.BaseConnector`] The connector to use for connection pooling. proxy: Optional[:class:`str`] @@ -245,7 +251,7 @@ def __init__( # self.ws is set in the connect method self.ws: DiscordWebSocket = None # type: ignore self.loop: asyncio.AbstractEventLoop = ( - asyncio.get_event_loop() if loop is None else loop + _get_event_loop() if loop is None else loop ) self._listeners: dict[str, list[tuple[asyncio.Future, Callable[..., bool]]]] = ( {} diff --git a/discord/ext/commands/cooldowns.py b/discord/ext/commands/cooldowns.py index 6e58d37f7a..7504eed7c8 100644 --- a/discord/ext/commands/cooldowns.py +++ b/discord/ext/commands/cooldowns.py @@ -32,6 +32,7 @@ import discord.abc from discord.enums import Enum +from discord.utils import _get_event_loop from ...abc import PrivateChannel from .errors import MaxConcurrencyReached @@ -308,7 +309,7 @@ class _Semaphore: def __init__(self, number: int) -> None: self.value: int = number - self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() + self.loop: asyncio.AbstractEventLoop = _get_event_loop() self._waiters: Deque[asyncio.Future] = deque() def __repr__(self) -> str: diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index 9bdde87f23..6bb719c67f 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -38,7 +38,7 @@ import discord from discord.backoff import ExponentialBackoff -from discord.utils import MISSING +from discord.utils import MISSING, _get_event_loop __all__ = ("loop",) @@ -384,7 +384,7 @@ def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]: args = (self._injected, *args) if self.loop is MISSING: - self.loop = asyncio.get_event_loop() + self.loop = _get_event_loop() self._task = self.loop.create_task(self._loop(*args, **kwargs)) return self._task @@ -825,8 +825,8 @@ def loop( using an exponential back-off algorithm similar to the one used in :meth:`discord.Client.connect`. loop: :class:`asyncio.AbstractEventLoop` - The loop to use to register the task, if not given - defaults to :func:`asyncio.get_event_loop`. + The loop to use to register the task, if not given the default event loop is used via + :func:`asyncio.get_event_loop()` if it exists or one is created via :func:`asyncio.new_event_loop()`. overlap: Union[:class:`bool`, :class:`int`] Controls whether overlapping executions of the task loop are allowed. Set to False (default) to run iterations one at a time, True for unlimited overlap, or an int to cap the number of concurrent runs. diff --git a/discord/http.py b/discord/http.py index 0717feadf5..fc6ffb22c3 100644 --- a/discord/http.py +++ b/discord/http.py @@ -55,7 +55,7 @@ from .file import VoiceMessage from .gateway import DiscordClientWebSocketResponse from .soundboard import PartialSoundboardSound, SoundboardSound -from .utils import MISSING +from .utils import MISSING, _get_event_loop _log = logging.getLogger(__name__) @@ -192,7 +192,7 @@ def __init__( unsync_clock: bool = True, ) -> None: self.loop: asyncio.AbstractEventLoop = ( - asyncio.get_event_loop() if loop is None else loop + _get_event_loop() if loop is None else loop ) self.connector = connector self.__session: aiohttp.ClientSession = MISSING # filled in static_login diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 6bb3a21d6a..67c68913f9 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -33,7 +33,7 @@ from typing import TYPE_CHECKING, Any, Iterator, TypeVar from ..enums import ComponentType -from ..utils import find +from ..utils import _get_event_loop, find from .core import ItemInterface from .input_text import InputText from .item import ModalItem @@ -91,7 +91,7 @@ def __init__( for item in children: self.add_item(item) self._title = title - self.loop = asyncio.get_event_loop() + self.loop = _get_event_loop() def __repr__(self) -> str: attrs = " ".join( diff --git a/discord/utils.py b/discord/utils.py index cc6d9d3b19..d2e382fc53 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -1665,3 +1665,21 @@ def warn_if_voice_dependencies_missing() -> None: deps, "is" if len(missing) == 1 else "are", ) + + +def _get_event_loop() -> asyncio.AbstractEventLoop: + """Get the current event loop, creating one if necessary. + + Returns + ------- + asyncio.AbstractEventLoop + The current event loop. + """ + if sys.version_info >= (3, 14): + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop + return asyncio.get_event_loop() From d89a7455a9bc7ce4743dfea9d8ed4f18319a6be5 Mon Sep 17 00:00:00 2001 From: Paillat Date: Sat, 28 Mar 2026 00:17:24 +0100 Subject: [PATCH 2/4] docs: better --- discord/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/discord/utils.py b/discord/utils.py index d2e382fc53..9caa5b19a9 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -1670,6 +1670,9 @@ def warn_if_voice_dependencies_missing() -> None: def _get_event_loop() -> asyncio.AbstractEventLoop: """Get the current event loop, creating one if necessary. + If no event loop is running and none is set, a new event loop + is created and set as the current event loop. + Returns ------- asyncio.AbstractEventLoop From 516c3280f0d4bcd30ae0105e059df600913a1b7b Mon Sep 17 00:00:00 2001 From: Paillat Date: Sat, 28 Mar 2026 00:19:53 +0100 Subject: [PATCH 3/4] docs: Refactor basic_voice example to use `asyncio.to_thread` for better compatibility with Python 3.14+ --- examples/basic_voice.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/basic_voice.py b/examples/basic_voice.py index 2525415762..f81c7ad373 100644 --- a/examples/basic_voice.py +++ b/examples/basic_voice.py @@ -42,11 +42,8 @@ def __init__(self, source: discord.AudioSource, *, data: dict, volume: float = 0 self.url = data.get("url") @classmethod - async def from_url(cls, url, *, loop=None, stream=False): - loop = loop or asyncio.get_event_loop() - data = await loop.run_in_executor( - None, lambda: ytdl.extract_info(url, download=not stream) - ) + async def from_url(cls, url, *, stream=False): + data = await asyncio.to_thread(ytdl.extract_info, url, download=not stream) if "entries" in data: # Takes the first item from a playlist From f82645f42c2187b29e699abedd49c4846a1631bb Mon Sep 17 00:00:00 2001 From: Paillat Date: Sun, 29 Mar 2026 21:06:41 +0200 Subject: [PATCH 4/4] fix: The patch is for get_event_loop not get_running_loop --- discord/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/utils.py b/discord/utils.py index 9caa5b19a9..d09551f508 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -1680,7 +1680,7 @@ def _get_event_loop() -> asyncio.AbstractEventLoop: """ if sys.version_info >= (3, 14): try: - loop = asyncio.get_running_loop() + loop = asyncio.get_event_loop() except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop)