diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py
index 7e62d1a5be1a..c49a301b6a24 100644
--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -2840,6 +2840,53 @@ def create_with_canvas(cls, canvas_class, figure, num):
         """
         return cls(canvas_class(figure), num)
 
+    @classmethod
+    def start_main_loop(cls):
+        """
+        Start the main event loop.
+
+        This method is called by `.FigureManagerBase.pyplot_show`, which is the
+        implementation of `.pyplot.show`.  To customize the behavior of
+        `.pyplot.show`, interactive backends should usually override
+        `~.FigureManagerBase.start_main_loop`; if more customized logic is
+        necessary, `~.FigureManagerBase.pyplot_show` can also be overridden.
+        """
+
+    @classmethod
+    def pyplot_show(cls, *, block=None):
+        """
+        Show all figures.  This method is the implementation of `.pyplot.show`.
+
+        To customize the behavior of `.pyplot.show`, interactive backends
+        should usually override `~.FigureManagerBase.start_main_loop`; if more
+        customized logic is necessary, `~.FigureManagerBase.pyplot_show` can
+        also be overridden.
+
+        Parameters
+        ----------
+        block : bool, optional
+            Whether to block by calling ``start_main_loop``.  The default,
+            None, means to block if we are neither in IPython's ``%pylab`` mode
+            nor in ``interactive`` mode.
+        """
+        managers = Gcf.get_all_fig_managers()
+        if not managers:
+            return
+        for manager in managers:
+            try:
+                manager.show()  # Emits a warning for non-interactive backend.
+            except NonGuiException as exc:
+                _api.warn_external(str(exc))
+        if block is None:
+            # Hack: Are we in IPython's %pylab mode?  In pylab mode, IPython
+            # (>= 0.10) tacks a _needmain attribute onto pyplot.show (always
+            # set to False).
+            ipython_pylab = hasattr(
+                getattr(sys.modules.get("pyplot"), "show", None), "_needmain")
+            block = not ipython_pylab and not is_interactive()
+        if block:
+            cls.start_main_loop()
+
     def show(self):
         """
         For GUI backends, show the figure window and redraw.
@@ -3518,7 +3565,12 @@ def new_figure_manager_given_figure(cls, num, figure):
 
     @classmethod
     def draw_if_interactive(cls):
-        if cls.mainloop is not None and is_interactive():
+        manager_class = cls.FigureCanvas.manager_class
+        # Interactive backends reimplement start_main_loop or pyplot_show.
+        backend_is_interactive = (
+            manager_class.start_main_loop != FigureManagerBase.start_main_loop
+            or manager_class.pyplot_show != FigureManagerBase.pyplot_show)
+        if backend_is_interactive and is_interactive():
             manager = Gcf.get_active()
             if manager:
                 manager.canvas.draw_idle()
@@ -3546,8 +3598,8 @@ def show(cls, *, block=None):
             # Hack: Are we in IPython's %pylab mode?  In pylab mode, IPython
             # (>= 0.10) tacks a _needmain attribute onto pyplot.show (always
             # set to False).
-            from matplotlib import pyplot
-            ipython_pylab = hasattr(pyplot.show, "_needmain")
+            ipython_pylab = hasattr(
+                getattr(sys.modules.get("pyplot"), "show", None), "_needmain")
             block = not ipython_pylab and not is_interactive()
         if block:
             cls.mainloop()
diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py
index 7eda5b924268..1fadc49a0d37 100644
--- a/lib/matplotlib/backends/_backend_gtk.py
+++ b/lib/matplotlib/backends/_backend_gtk.py
@@ -9,7 +9,8 @@
 from matplotlib import _api, backend_tools, cbook
 from matplotlib._pylab_helpers import Gcf
 from matplotlib.backend_bases import (
-    _Backend, FigureManagerBase, NavigationToolbar2, TimerBase)
+    _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
+    TimerBase)
 from matplotlib.backend_tools import Cursors
 
 import gi
@@ -113,6 +114,10 @@ def _on_timer(self):
             return False
 
 
+class _FigureCanvasGTK(FigureCanvasBase):
+    _timer_cls = TimerGTK
+
+
 class _FigureManagerGTK(FigureManagerBase):
     """
     Attributes
@@ -192,6 +197,25 @@ def destroy(self, *args):
         self.window.destroy()
         self.canvas.destroy()
 
+    @classmethod
+    def start_main_loop(cls):
+        global _application
+        if _application is None:
+            return
+
+        try:
+            _application.run()  # Quits when all added windows close.
+        except KeyboardInterrupt:
+            # Ensure all windows can process their close event from
+            # _shutdown_application.
+            context = GLib.MainContext.default()
+            while context.pending():
+                context.iteration(True)
+            raise
+        finally:
+            # Running after quit is undefined, so create a new one next time.
+            _application = None
+
     def show(self):
         # show the figure window
         self.window.show()
@@ -305,22 +329,4 @@ class _BackendGTK(_Backend):
         Gtk.get_minor_version(),
         Gtk.get_micro_version(),
     )
-
-    @staticmethod
-    def mainloop():
-        global _application
-        if _application is None:
-            return
-
-        try:
-            _application.run()  # Quits when all added windows close.
-        except KeyboardInterrupt:
-            # Ensure all windows can process their close event from
-            # _shutdown_application.
-            context = GLib.MainContext.default()
-            while context.pending():
-                context.iteration(True)
-            raise
-        finally:
-            # Running after quit is undefined, so create a new one next time.
-            _application = None
+    mainloop = _FigureManagerGTK.start_main_loop
diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py
index 07aacd74eb5b..2a691f55b974 100644
--- a/lib/matplotlib/backends/_backend_tk.py
+++ b/lib/matplotlib/backends/_backend_tk.py
@@ -484,6 +484,20 @@ def create_with_canvas(cls, canvas_class, figure, num):
                 canvas.draw_idle()
             return manager
 
+    @classmethod
+    def start_main_loop(cls):
+        managers = Gcf.get_all_fig_managers()
+        if managers:
+            first_manager = managers[0]
+            manager_class = type(first_manager)
+            if manager_class._owns_mainloop:
+                return
+            manager_class._owns_mainloop = True
+            try:
+                first_manager.window.mainloop()
+            finally:
+                manager_class._owns_mainloop = False
+
     def _update_window_dpi(self, *args):
         newdpi = self._window_dpi.get()
         self.window.call('tk', 'scaling', newdpi / 72)
@@ -1018,18 +1032,6 @@ def trigger(self, *args):
 @_Backend.export
 class _BackendTk(_Backend):
     backend_version = tk.TkVersion
+    FigureCanvas = FigureCanvasTk
     FigureManager = FigureManagerTk
-
-    @staticmethod
-    def mainloop():
-        managers = Gcf.get_all_fig_managers()
-        if managers:
-            first_manager = managers[0]
-            manager_class = type(first_manager)
-            if manager_class._owns_mainloop:
-                return
-            manager_class._owns_mainloop = True
-            try:
-                first_manager.window.mainloop()
-            finally:
-                manager_class._owns_mainloop = False
+    mainloop = FigureManagerTk.start_main_loop
diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py
index 2169a58de4de..4994e434987d 100644
--- a/lib/matplotlib/backends/backend_gtk3.py
+++ b/lib/matplotlib/backends/backend_gtk3.py
@@ -7,8 +7,8 @@
 import matplotlib as mpl
 from matplotlib import _api, backend_tools, cbook
 from matplotlib.backend_bases import (
-    FigureCanvasBase, ToolContainerBase,
-    CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
+    ToolContainerBase, CloseEvent, KeyEvent, LocationEvent, MouseEvent,
+    ResizeEvent)
 
 try:
     import gi
@@ -26,8 +26,8 @@
 
 from gi.repository import Gio, GLib, GObject, Gtk, Gdk
 from . import _backend_gtk
-from ._backend_gtk import (
-    _BackendGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
+from ._backend_gtk import (  # noqa: F401 # pylint: disable=W0611
+    _BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
     TimerGTK as TimerGTK3,
 )
 
@@ -52,9 +52,8 @@ def _mpl_to_gtk_cursor(mpl_cursor):
         _backend_gtk.mpl_to_gtk_cursor_name(mpl_cursor))
 
 
-class FigureCanvasGTK3(FigureCanvasBase, Gtk.DrawingArea):
+class FigureCanvasGTK3(_FigureCanvasGTK, Gtk.DrawingArea):
     required_interactive_framework = "gtk3"
-    _timer_cls = TimerGTK3
     manager_class = _api.classproperty(lambda cls: FigureManagerGTK3)
     # Setting this as a static constant prevents
     # this resulting expression from leaking
diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py
index 923787150a8d..c776fa7244c2 100644
--- a/lib/matplotlib/backends/backend_gtk4.py
+++ b/lib/matplotlib/backends/backend_gtk4.py
@@ -5,8 +5,7 @@
 import matplotlib as mpl
 from matplotlib import _api, backend_tools, cbook
 from matplotlib.backend_bases import (
-    FigureCanvasBase, ToolContainerBase,
-    KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
+    ToolContainerBase, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
 
 try:
     import gi
@@ -24,16 +23,15 @@
 
 from gi.repository import Gio, GLib, Gtk, Gdk, GdkPixbuf
 from . import _backend_gtk
-from ._backend_gtk import (
-    _BackendGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
+from ._backend_gtk import (  # noqa: F401 # pylint: disable=W0611
+    _BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
     TimerGTK as TimerGTK4,
 )
 
 
-class FigureCanvasGTK4(FigureCanvasBase, Gtk.DrawingArea):
+class FigureCanvasGTK4(_FigureCanvasGTK, Gtk.DrawingArea):
     required_interactive_framework = "gtk4"
     supports_blit = False
-    _timer_cls = TimerGTK4
     manager_class = _api.classproperty(lambda cls: FigureManagerGTK4)
     _context_is_scaled = False
 
diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py
index 10dff3ef33f1..2867c13f430b 100644
--- a/lib/matplotlib/backends/backend_macosx.py
+++ b/lib/matplotlib/backends/backend_macosx.py
@@ -165,6 +165,10 @@ def _close_button_pressed(self):
     def close(self):
         return self._close_button_pressed()
 
+    @classmethod
+    def start_main_loop(cls):
+        _macosx.show()
+
     def show(self):
         if not self._shown:
             self._show()
@@ -177,7 +181,4 @@ def show(self):
 class _BackendMac(_Backend):
     FigureCanvas = FigureCanvasMac
     FigureManager = FigureManagerMac
-
-    @staticmethod
-    def mainloop():
-        _macosx.show()
+    mainloop = FigureManagerMac.start_main_loop
diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py
index c133dfd079c7..8f602d71d7c8 100644
--- a/lib/matplotlib/backends/backend_qt.py
+++ b/lib/matplotlib/backends/backend_qt.py
@@ -583,6 +583,13 @@ def resize(self, width, height):
         self.canvas.resize(width, height)
         self.window.resize(width + extra_width, height + extra_height)
 
+    @classmethod
+    def start_main_loop(cls):
+        qapp = QtWidgets.QApplication.instance()
+        if qapp:
+            with _maybe_allow_interrupt(qapp):
+                qt_compat._exec(qapp)
+
     def show(self):
         self.window.show()
         if mpl.rcParams['figure.raise_window']:
@@ -1007,9 +1014,4 @@ class _BackendQT(_Backend):
     backend_version = __version__
     FigureCanvas = FigureCanvasQT
     FigureManager = FigureManagerQT
-
-    @staticmethod
-    def mainloop():
-        qapp = QtWidgets.QApplication.instance()
-        with _maybe_allow_interrupt(qapp):
-            qt_compat._exec(qapp)
+    mainloop = FigureManagerQT.start_main_loop
diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py
index 4eac97ff9c35..39906e53eefa 100644
--- a/lib/matplotlib/backends/backend_webagg.py
+++ b/lib/matplotlib/backends/backend_webagg.py
@@ -53,6 +53,24 @@ def run(self):
 class FigureManagerWebAgg(core.FigureManagerWebAgg):
     _toolbar2_class = core.NavigationToolbar2WebAgg
 
+    @classmethod
+    def pyplot_show(cls, *, block=None):
+        WebAggApplication.initialize()
+
+        url = "http://{address}:{port}{prefix}".format(
+            address=WebAggApplication.address,
+            port=WebAggApplication.port,
+            prefix=WebAggApplication.url_prefix)
+
+        if mpl.rcParams['webagg.open_in_browser']:
+            import webbrowser
+            if not webbrowser.open(url):
+                print("To view figure, visit {0}".format(url))
+        else:
+            print("To view figure, visit {0}".format(url))
+
+        WebAggApplication.start()
+
 
 class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
     manager_class = FigureManagerWebAgg
@@ -307,21 +325,3 @@ def ipython_inline_display(figure):
 class _BackendWebAgg(_Backend):
     FigureCanvas = FigureCanvasWebAgg
     FigureManager = FigureManagerWebAgg
-
-    @staticmethod
-    def show(*, block=None):
-        WebAggApplication.initialize()
-
-        url = "http://{address}:{port}{prefix}".format(
-            address=WebAggApplication.address,
-            port=WebAggApplication.port,
-            prefix=WebAggApplication.url_prefix)
-
-        if mpl.rcParams['webagg.open_in_browser']:
-            import webbrowser
-            if not webbrowser.open(url):
-                print("To view figure, visit {0}".format(url))
-        else:
-            print("To view figure, visit {0}".format(url))
-
-        WebAggApplication.start()
diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py
index 84bfa2eed031..226013595dce 100644
--- a/lib/matplotlib/backends/backend_wx.py
+++ b/lib/matplotlib/backends/backend_wx.py
@@ -999,6 +999,13 @@ def create_with_canvas(cls, canvas_class, figure, num):
             figure.canvas.draw_idle()
         return manager
 
+    @classmethod
+    def start_main_loop(cls):
+        if not wx.App.IsMainLoopRunning():
+            wxapp = wx.GetApp()
+            if wxapp is not None:
+                wxapp.MainLoop()
+
     def show(self):
         # docstring inherited
         self.frame.Show()
@@ -1365,10 +1372,4 @@ def trigger(self, *args, **kwargs):
 class _BackendWx(_Backend):
     FigureCanvas = FigureCanvasWx
     FigureManager = FigureManagerWx
-
-    @staticmethod
-    def mainloop():
-        if not wx.App.IsMainLoopRunning():
-            wxapp = wx.GetApp()
-            if wxapp is not None:
-                wxapp.MainLoop()
+    mainloop = FigureManagerWx.start_main_loop
diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py
index 93d2171e9c68..b37e43833318 100644
--- a/lib/matplotlib/pyplot.py
+++ b/lib/matplotlib/pyplot.py
@@ -56,7 +56,8 @@
 from matplotlib import _pylab_helpers, interactive
 from matplotlib import cbook
 from matplotlib import _docstring
-from matplotlib.backend_bases import FigureCanvasBase, MouseButton
+from matplotlib.backend_bases import (
+    FigureCanvasBase, FigureManagerBase, MouseButton)
 from matplotlib.figure import Figure, FigureBase, figaspect
 from matplotlib.gridspec import GridSpec, SubplotSpec
 from matplotlib import rcParams, rcParamsDefault, get_backend, rcParamsOrig
@@ -280,7 +281,8 @@ def switch_backend(newbackend):
     # Classically, backends can directly export these functions.  This should
     # keep working for backcompat.
     new_figure_manager = getattr(backend_mod, "new_figure_manager", None)
-    # show = getattr(backend_mod, "show", None)
+    show = getattr(backend_mod, "show", None)
+
     # In that classical approach, backends are implemented as modules, but
     # "inherit" default method implementations from backend_bases._Backend.
     # This is achieved by creating a "class" that inherits from
@@ -288,13 +290,14 @@ def switch_backend(newbackend):
     class backend_mod(matplotlib.backend_bases._Backend):
         locals().update(vars(backend_mod))
 
-    # However, the newer approach for defining new_figure_manager (and, in
-    # the future, show) is to derive them from canvas methods.  In that case,
-    # also update backend_mod accordingly; also, per-backend customization of
+    # However, the newer approach for defining new_figure_manager and
+    # show is to derive them from canvas methods.  In that case, also
+    # update backend_mod accordingly; also, per-backend customization of
     # draw_if_interactive is disabled.
     if new_figure_manager is None:
-        # only try to get the canvas class if have opted into the new scheme
+        # Only try to get the canvas class if have opted into the new scheme.
         canvas_class = backend_mod.FigureCanvas
+
         def new_figure_manager_given_figure(num, figure):
             return canvas_class.new_manager(figure, num)
 
@@ -313,6 +316,14 @@ def draw_if_interactive():
         backend_mod.new_figure_manager = new_figure_manager
         backend_mod.draw_if_interactive = draw_if_interactive
 
+    # If the manager explicitly overrides pyplot_show, use it even if a global
+    # show is already present, as the latter may be here for backcompat.
+    manager_class = getattr(getattr(backend_mod, "FigureCanvas", None),
+                            "manager_class", None)
+    if (manager_class.pyplot_show != FigureManagerBase.pyplot_show
+            or show is None):
+        backend_mod.show = manager_class.pyplot_show
+
     _log.debug("Loaded backend %s version %s.",
                newbackend, backend_mod.backend_version)
 
diff --git a/lib/matplotlib/tests/test_backend_template.py b/lib/matplotlib/tests/test_backend_template.py
index 7afe8bae69a9..1bc666ce1814 100644
--- a/lib/matplotlib/tests/test_backend_template.py
+++ b/lib/matplotlib/tests/test_backend_template.py
@@ -4,6 +4,7 @@
 
 import sys
 from types import SimpleNamespace
+from unittest.mock import MagicMock
 
 import matplotlib as mpl
 from matplotlib import pyplot as plt
@@ -27,3 +28,14 @@ def test_load_old_api(monkeypatch):
     mpl.use("module://mpl_test_backend")
     assert type(plt.figure().canvas) == FigureCanvasTemplate
     plt.draw_if_interactive()
+
+
+def test_show(monkeypatch):
+    mpl_test_backend = SimpleNamespace(**vars(backend_template))
+    mock_show = backend_template.FigureManagerTemplate.pyplot_show = \
+        MagicMock()
+    del mpl_test_backend.show
+    monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend)
+    mpl.use("module://mpl_test_backend")
+    plt.show()
+    mock_show.assert_called_with()