From 3f937281e87b68f3a42ca178db67d9a677a0a332 Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:30:44 +0100 Subject: [PATCH 01/30] Backport PR #30788: Fix typo in key-mapping for "f11" (#30791) Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- lib/matplotlib/backends/backend_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index d0aded5fff63..dd642ba838af 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -55,7 +55,7 @@ ("Key_F8", "f8"), ("Key_F9", "f9"), ("Key_F10", "f10"), - ("Key_F10", "f11"), + ("Key_F11", "f11"), ("Key_F12", "f12"), ("Key_Super_L", "super"), ("Key_Super_R", "super"), From 6a38e4e0725f5ca0310c098d674edce3f5947d5f Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 29 Nov 2025 21:19:05 +0000 Subject: [PATCH 02/30] Backport PR #30763: DOC: Add example how to align tick labels --- galleries/examples/ticks/align_ticklabels.py | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 galleries/examples/ticks/align_ticklabels.py diff --git a/galleries/examples/ticks/align_ticklabels.py b/galleries/examples/ticks/align_ticklabels.py new file mode 100644 index 000000000000..ec36e0db4d07 --- /dev/null +++ b/galleries/examples/ticks/align_ticklabels.py @@ -0,0 +1,32 @@ +""" +================= +Align tick labels +================= + +By default, tick labels are aligned towards the axis. This means the set of +*y* tick labels appear right-aligned. Because the alignment reference point +is on the axis, left-aligned tick labels would overlap the plotting area. +To achieve a good-looking left-alignment, you have to additionally increase +the padding. +""" +import matplotlib.pyplot as plt + +population = { + "Sydney": 5.2, + "Mexico City": 8.8, + "São Paulo": 12.2, + "Istanbul": 15.9, + "Lagos": 15.9, + "Shanghai": 21.9, +} + +fig, ax = plt.subplots(layout="constrained") +ax.barh(population.keys(), population.values()) +ax.set_xlabel('Population (in millions)') + +# left-align all ticklabels +for ticklabel in ax.get_yticklabels(): + ticklabel.set_horizontalalignment("left") + +# increase padding +ax.tick_params("y", pad=70) From ce645296e2ce963e85d254e869238c200635d7be Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 7 Dec 2025 15:30:40 +0100 Subject: [PATCH 03/30] Backport PR #30817: Update sphinx-gallery header patch --- doc/conf.py | 2 +- lib/matplotlib/tests/test_doc.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 96738492b68b..03df9340e66c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -364,7 +364,7 @@ def gallery_image_warning_filter(record): :class: sphx-glr-download-link-note :ref:`Go to the end ` - to download the full example code.{2} + to download the full example code{2} .. rst-class:: sphx-glr-example-title diff --git a/lib/matplotlib/tests/test_doc.py b/lib/matplotlib/tests/test_doc.py index 3e28fd1b8eb7..6e8ce9fd630c 100644 --- a/lib/matplotlib/tests/test_doc.py +++ b/lib/matplotlib/tests/test_doc.py @@ -9,7 +9,7 @@ def test_sphinx_gallery_example_header(): EXAMPLE_HEADER, this test will start to fail. In that case, please update the monkey-patching of EXAMPLE_HEADER in conf.py. """ - pytest.importorskip('sphinx_gallery', minversion='0.16.0') + pytest.importorskip('sphinx_gallery', minversion='0.20.0') from sphinx_gallery import gen_rst EXAMPLE_HEADER = """ @@ -25,7 +25,7 @@ def test_sphinx_gallery_example_header(): :class: sphx-glr-download-link-note :ref:`Go to the end ` - to download the full example code.{2} + to download the full example code{2} .. rst-class:: sphx-glr-example-title From 1957ba391889a4d6b9048b77080f62b1a1dbcad0 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 12 Dec 2025 12:24:39 -0600 Subject: [PATCH 04/30] Zenodo v3.10.8 --- doc/_static/zenodo_cache/17595503.svg | 35 +++++++++++++++++++++++++++ doc/project/citing.rst | 3 +++ tools/cache_zenodo_svg.py | 1 + 3 files changed, 39 insertions(+) create mode 100644 doc/_static/zenodo_cache/17595503.svg diff --git a/doc/_static/zenodo_cache/17595503.svg b/doc/_static/zenodo_cache/17595503.svg new file mode 100644 index 000000000000..891bd118d125 --- /dev/null +++ b/doc/_static/zenodo_cache/17595503.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.17595503 + + + 10.5281/zenodo.17595503 + + + \ No newline at end of file diff --git a/doc/project/citing.rst b/doc/project/citing.rst index c5e56e6f12d4..1f8c6509e9e6 100644 --- a/doc/project/citing.rst +++ b/doc/project/citing.rst @@ -32,6 +32,9 @@ By version .. START OF AUTOGENERATED +v3.10.8 + .. image:: ../_static/zenodo_cache/17595503.svg + :target: https://doi.org/10.5281/zenodo.17595503 v3.10.7 .. image:: ../_static/zenodo_cache/17298696.svg :target: https://doi.org/10.5281/zenodo.17298696 diff --git a/tools/cache_zenodo_svg.py b/tools/cache_zenodo_svg.py index 07b67a3e04ee..2ee72c6a89fa 100644 --- a/tools/cache_zenodo_svg.py +++ b/tools/cache_zenodo_svg.py @@ -63,6 +63,7 @@ def _get_xdg_cache_dir(): if __name__ == "__main__": data = { + "v3.10.8": "17595503", "v3.10.7": "17298696", "v3.10.6": "16999430", "v3.10.5": "16644850", From 9d0781d25f363466c71898dabb1e1abffe14c504 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 15 Dec 2025 09:41:14 -0500 Subject: [PATCH 05/30] Backport PR #30858: DOC: reinstate "codex" search term --- doc/conf.py | 29 +---------------------------- doc/sphinxext/util.py | 32 ++++++++++++++++++++++++++++++++ lib/matplotlib/tests/test_doc.py | 2 +- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 03df9340e66c..292aff3e1983 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -200,8 +200,6 @@ def _check_dependencies(): else: sg_matplotlib_animations = True -# The following import is only necessary to monkey patch the signature later on -from sphinx_gallery import gen_rst # Prevent plt.show() from emitting a non-GUI backend warning. warnings.filterwarnings('ignore', category=UserWarning, @@ -299,7 +297,7 @@ def autodoc_process_bases(app, name, obj, options, bases): 'reference_url': {'matplotlib': None, 'mpl_toolkits': None}, 'prefer_full_module': {r'mpl_toolkits\.'}, 'remove_config_comments': True, - 'reset_modules': ('matplotlib', clear_basic_units), + 'reset_modules': ('matplotlib', clear_basic_units, 'sphinxext.util.patch_header'), 'subsection_order': gallery_order_sectionorder, 'thumbnail_size': (320, 224), 'within_subsection_order': gallery_order_subsectionorder, @@ -347,31 +345,6 @@ def gallery_image_warning_filter(record): mathmpl_fontsize = 11.0 mathmpl_srcset = ['2x'] -# Monkey-patching gallery header to include search keywords -gen_rst.EXAMPLE_HEADER = """ -.. DO NOT EDIT. -.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. -.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: -.. "{0}" -.. LINE NUMBERS ARE GIVEN BELOW. - -.. only:: html - - .. meta:: - :keywords: codex - - .. note:: - :class: sphx-glr-download-link-note - - :ref:`Go to the end ` - to download the full example code{2} - -.. rst-class:: sphx-glr-example-title - -.. _sphx_glr_{1}: - -""" - # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/doc/sphinxext/util.py b/doc/sphinxext/util.py index 14097ba9396a..c0f336eaea18 100644 --- a/doc/sphinxext/util.py +++ b/doc/sphinxext/util.py @@ -1,5 +1,7 @@ import sys +from sphinx_gallery import gen_rst + def matplotlib_reduced_latex_scraper(block, block_vars, gallery_conf, **kwargs): @@ -19,3 +21,33 @@ def matplotlib_reduced_latex_scraper(block, block_vars, gallery_conf, # Clear basic_units module to re-register with unit registry on import. def clear_basic_units(gallery_conf, fname): return sys.modules.pop('basic_units', None) + + +# Monkey-patching gallery header to include search keywords +EXAMPLE_HEADER = """ +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "{0}" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. meta:: + :keywords: codex + + .. note:: + :class: sphx-glr-download-link-note + + :ref:`Go to the end ` + to download the full example code{2} + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_{1}: + +""" + + +def patch_header(gallery_conf, fname): + gen_rst.EXAMPLE_HEADER = EXAMPLE_HEADER diff --git a/lib/matplotlib/tests/test_doc.py b/lib/matplotlib/tests/test_doc.py index 6e8ce9fd630c..f3d6d6e3fd5d 100644 --- a/lib/matplotlib/tests/test_doc.py +++ b/lib/matplotlib/tests/test_doc.py @@ -7,7 +7,7 @@ def test_sphinx_gallery_example_header(): This test monitors that the version we have copied is still the same as the EXAMPLE_HEADER in sphinx-gallery. If sphinx-gallery changes its EXAMPLE_HEADER, this test will start to fail. In that case, please update - the monkey-patching of EXAMPLE_HEADER in conf.py. + the monkey-patching of EXAMPLE_HEADER in sphinxext/util.py. """ pytest.importorskip('sphinx_gallery', minversion='0.20.0') from sphinx_gallery import gen_rst From 44431a6e9439606c93aa8178da49b40fa2dbe4d6 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 2 Jan 2026 21:57:18 +0100 Subject: [PATCH 06/30] Backport PR #30910: DOC: Improve writer parameter docs of Animation.save() --- lib/matplotlib/animation.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index a87f00201124..a4eb80ad1b34 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -951,9 +951,21 @@ def save(self, filename, writer=None, fps=None, dpi=None, codec=None, filename : str The output filename, e.g., :file:`mymovie.mp4`. - writer : `MovieWriter` or str, default: :rc:`animation.writer` - A `MovieWriter` instance to use or a key that identifies a - class to use, such as 'ffmpeg'. + writer : `AbstractMovieWriter` subclass or str, default: :rc:`animation.writer` + The writer used to grab the frames and create the movie file. + This can be an instance of an `AbstractMovieWriter` subclass or a + string. The builtin writers are + + ================== ============================== + str class + ================== ============================== + 'ffmpeg' `.FFMpegWriter` + 'ffmpeg_file' `.FFMpegFileWriter` + 'imagemagick' `.ImageMagickWriter` + 'imagemagick_file' `.ImageMagickFileWriter` + 'pillow' `.PillowWriter` + 'html' `.HTMLWriter` + ================== ============================== fps : int, optional Movie frame rate (per second). If not set, the frame rate from the From a59d1d27c90d1bb7da203251febca72ea04f0eaa Mon Sep 17 00:00:00 2001 From: Steve Berardi <6608085+steveberardi@users.noreply.github.com> Date: Wed, 14 Jan 2026 09:28:51 -0800 Subject: [PATCH 07/30] Backport PR #30960: SVG backend - handle font weight as integer --- lib/matplotlib/backends/backend_svg.py | 3 ++- lib/matplotlib/font_manager.py | 2 +- lib/matplotlib/tests/test_backend_svg.py | 6 ++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 2193dc6b6cdc..7789ec2cd882 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1132,7 +1132,8 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): font_style['font-style'] = prop.get_style() if prop.get_variant() != 'normal': font_style['font-variant'] = prop.get_variant() - weight = fm.weight_dict[prop.get_weight()] + weight = prop.get_weight() + weight = fm.weight_dict.get(weight, weight) # convert to int if weight != 400: font_style['font-weight'] = f'{weight}' diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 9aa8dccde444..a44d5bd76b9a 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -744,7 +744,7 @@ def get_variant(self): def get_weight(self): """ - Set the font weight. Options are: A numeric value in the + Get the font weight. Options are: A numeric value in the range 0-1000 or one of 'light', 'normal', 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', 'extra bold', 'black' diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 509136fe323e..0f9f5f19afb5 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -73,7 +73,8 @@ def test_bold_font_output(): ax.plot(np.arange(10), np.arange(10)) ax.set_xlabel('nonbold-xlabel') ax.set_ylabel('bold-ylabel', fontweight='bold') - ax.set_title('bold-title', fontweight='bold') + # set weight as integer to assert it's handled properly + ax.set_title('bold-title', fontweight=600) @image_comparison(['bold_font_output_with_none_fonttype.svg']) @@ -83,7 +84,8 @@ def test_bold_font_output_with_none_fonttype(): ax.plot(np.arange(10), np.arange(10)) ax.set_xlabel('nonbold-xlabel') ax.set_ylabel('bold-ylabel', fontweight='bold') - ax.set_title('bold-title', fontweight='bold') + # set weight as integer to assert it's handled properly + ax.set_title('bold-title', fontweight=600) @check_figures_equal(tol=20) From bc4431823a9d02dd35367c04aee0a9a4d8ad5c6d Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:07:29 +0100 Subject: [PATCH 08/30] Backport PR #30952: DOC: Tutorial on API shortcuts --- galleries/tutorials/coding_shortcuts.py | 172 ++++++++++++++++++++++++ galleries/tutorials/index.rst | 18 +++ 2 files changed, 190 insertions(+) create mode 100644 galleries/tutorials/coding_shortcuts.py diff --git a/galleries/tutorials/coding_shortcuts.py b/galleries/tutorials/coding_shortcuts.py new file mode 100644 index 000000000000..46868482598f --- /dev/null +++ b/galleries/tutorials/coding_shortcuts.py @@ -0,0 +1,172 @@ +""" +================ +Coding shortcuts +================ + +Matplotlib's primary and universal API is the :ref:`Axes interface `. +While it is clearly structured and powerful, it can sometimes feel overly verbose and +thus cumbersome to write. This page collects patterns for condensing the code +of the Axes-based API and achieving the same results with less typing for many simpler +plots. + +.. note:: + + The :ref:`pyplot interface ` is an alternative more compact + interface, and was historically modeled to be similar to MATLAB. It remains a + valid approach for those who want to use it. However, it has the disadvantage that + it achieves its brevity through implicit assumptions that you have to understand. + + Since it follows a different paradigm, switching between the Axes interface and + the pyplot interface requires a shift of the mental model, and some code rewrite, + if the code develops to a point at which pyplot no longer provides enough + flexibility. + +This tutorial goes the other way round, starting from the standard verbose Axes +interface and using its capabilities for shortcuts when you don't need all the +generality. + +Let's assume we want to make a plot of the number of daylight hours per day over the +year in London. + +The standard approach with the Axes interface looks like this. +""" + +import matplotlib.pyplot as plt +import numpy as np + +day = np.arange(365) +hours = 4.276 * np.sin(2 * np.pi * (day - 80)/365) + 12.203 + +fig, ax = plt.subplots() +ax.plot(day, hours, color="orange") +ax.set_xlabel("day") +ax.set_ylabel("daylight hours") +ax.set_title("London") +plt.show() + +# %% +# Note that we've included ``plt.show()`` here. This is needed to show the plot window +# when running from a command line or in a Python script. If you run a Jupyter notebook, +# this command is automatically executed at the end of each cell. +# +# For the rest of the tutorial, we'll assume that we are in a notebook and leave this +# out for brevity. Depending on your context you may still need it. +# +# If you instead want to save to a file, use ``fig.savefig("daylight.png")``. +# +# +# Collect Axes properties into a single ``set()`` call +# ==================================================== +# +# The properties of Matplotlib Artists can be modified through their respective +# ``set_*()`` methods. Artists additionally have a generic ``set()`` method, that takes +# keyword arguments and is equivalent to calling all the respective ``set_*()`` methods. +# :: +# +# ax.set_xlabel("day") +# ax.set_ylabel("daylight hours") +# +# can also be written as :: +# +# ax.set(xlabel="day", ylabel="daylight hours") +# +# This is the most simple and effective reduction you can do. With that we can shorten +# the above plot to + +fig, ax = plt.subplots() +ax.plot(day, hours, color="orange") +ax.set(xlabel="day", ylabel="daylight hours", title="London") + +# %% +# +# This works as long as you only need to pass one parameter to the ``set_*()`` function. +# The individual functions are still necessary if you want more control, e.g. +# ``set_title("London", fontsize=16)``. +# +# +# Not storing a reference to the figure +# ===================================== +# Another nuisance of ``fig, ax = plt.subplots()`` is that you always create a ``fig`` +# variable, even if you don't use it. A slightly shorter version would be using the +# standard variable for unused value in Python (``_``): ``_, ax = plt.subplots()``. +# However, that's only marginally better. +# +# You can work around this by separating figure and Axes creation and chaining them :: +# +# ax = plt.figure().add_subplot() +# +# This is a bit cleaner logically and has the slight advantage that you could set +# figure properties inline as well; e.g. ``plt.figure(facecolor="lightgoldenrod")``. +# But it has the disadvantage that it's longer than ``fig, ax = plt.subplots()``. +# +# You can still obtain the figure from the Axes if needed, e.g. :: +# +# ax.figure.savefig("daylight_hours.png") +# +# The example code now looks like this: + +ax = plt.figure().add_subplot() +ax.plot(day, hours, color="orange") +ax.set(xlabel="day", ylabel="daylight hours", title="London") + +# %% +# Define Axes properties during axes creation +# =========================================== +# The ``set_*`` methods as well as ``set`` modify existing objects. You can +# alternatively define them right at creation. Since you typically don't instantiate +# classes yourself in Matplotlib, but rather call some factory function that creates +# the object and wires it up correctly with the plot, this may seem less obvious. But +# in fact you just pass the desired properties to the factory functions. You are likely +# doing this already in some places without realizing. Consider the function to create +# a line :: +# +# ax.plot(x, y, color="orange") +# +# This is equivalent to :: +# +# line, = ax.plot(x, y) +# line.set_color("orange") +# +# The same can be done with functions that create Axes. + +ax = plt.figure().add_subplot(xlabel="day", ylabel="daylight hours", title="London") +ax.plot(day, hours, color="orange") + +# %% +# .. important:: +# The Axes properties are only accepted as keyword arguments by +# `.Figure.add_subplot`, which creates a single Axes. +# +# For `.Figure.subplots` and `.pyplot.subplots`, you'd have to pass the properties +# as a dict via the keyword argument ``subplot_kw``. The limitation here is that +# such parameters are given to all Axes. For example, if you need two subplots +# (``fig, (ax1, ax2) = plt.subplots(1, 2)``) with different labels, you have to +# set them individually. +# +# Defining Axes properties during creation is best used for single subplots or when +# all subplots share the same properties. +# +# +# Using implicit figure creation +# ============================== +# You can go even further by tapping into the pyplot logic and use `.pyplot.axes` to +# create the axes: + +ax = plt.axes(xlabel="day", ylabel="daylight hours", title="London") +ax.plot(day, hours, color="orange") + +# %% +# .. warning:: +# When using this, you have to be aware of the implicit figure semantics of pyplot. +# ``plt.axes()`` will only create a new figure if no figure exists. Otherwise, it +# will add the Axes to the current existing figure, which is likely not what you +# want. +# +# Not storing a reference to the Axes +# =================================== +# If you only need to visualize one dataset, you can append the plot command +# directly to the Axes creation. This may be useful e.g. in notebooks, +# where you want to create a plot with some configuration, but as little distracting +# code as possible: + +plt.axes(xlabel="day", ylabel="daylight hours").plot(day, hours, color="orange") diff --git a/galleries/tutorials/index.rst b/galleries/tutorials/index.rst index 48187a862a2e..76c0037dca11 100644 --- a/galleries/tutorials/index.rst +++ b/galleries/tutorials/index.rst @@ -32,6 +32,23 @@ a :ref:`FAQ ` in our :ref:`user guide `. +.. raw:: html + +
+ +.. only:: html + + .. image:: /tutorials/images/thumb/sphx_glr_coding_shortcuts_thumb.png + :alt: Coding shortcuts + + :ref:`sphx_glr_tutorials_coding_shortcuts.py` + +.. raw:: html + +
Coding shortcuts
+
+ + .. raw:: html
@@ -92,6 +109,7 @@ a :ref:`FAQ ` in our :ref:`user guide `. :hidden: /tutorials/pyplot + /tutorials/coding_shortcuts /tutorials/images /tutorials/lifecycle /tutorials/artists From efb581cc82718c7c485674e6d886ffc7aa606d23 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:07:29 +0100 Subject: [PATCH 09/30] Backport PR #30952: DOC: Tutorial on API shortcuts --- galleries/tutorials/coding_shortcuts.py | 172 ++++++++++++++++++++++++ galleries/tutorials/index.rst | 18 +++ 2 files changed, 190 insertions(+) create mode 100644 galleries/tutorials/coding_shortcuts.py diff --git a/galleries/tutorials/coding_shortcuts.py b/galleries/tutorials/coding_shortcuts.py new file mode 100644 index 000000000000..46868482598f --- /dev/null +++ b/galleries/tutorials/coding_shortcuts.py @@ -0,0 +1,172 @@ +""" +================ +Coding shortcuts +================ + +Matplotlib's primary and universal API is the :ref:`Axes interface `. +While it is clearly structured and powerful, it can sometimes feel overly verbose and +thus cumbersome to write. This page collects patterns for condensing the code +of the Axes-based API and achieving the same results with less typing for many simpler +plots. + +.. note:: + + The :ref:`pyplot interface ` is an alternative more compact + interface, and was historically modeled to be similar to MATLAB. It remains a + valid approach for those who want to use it. However, it has the disadvantage that + it achieves its brevity through implicit assumptions that you have to understand. + + Since it follows a different paradigm, switching between the Axes interface and + the pyplot interface requires a shift of the mental model, and some code rewrite, + if the code develops to a point at which pyplot no longer provides enough + flexibility. + +This tutorial goes the other way round, starting from the standard verbose Axes +interface and using its capabilities for shortcuts when you don't need all the +generality. + +Let's assume we want to make a plot of the number of daylight hours per day over the +year in London. + +The standard approach with the Axes interface looks like this. +""" + +import matplotlib.pyplot as plt +import numpy as np + +day = np.arange(365) +hours = 4.276 * np.sin(2 * np.pi * (day - 80)/365) + 12.203 + +fig, ax = plt.subplots() +ax.plot(day, hours, color="orange") +ax.set_xlabel("day") +ax.set_ylabel("daylight hours") +ax.set_title("London") +plt.show() + +# %% +# Note that we've included ``plt.show()`` here. This is needed to show the plot window +# when running from a command line or in a Python script. If you run a Jupyter notebook, +# this command is automatically executed at the end of each cell. +# +# For the rest of the tutorial, we'll assume that we are in a notebook and leave this +# out for brevity. Depending on your context you may still need it. +# +# If you instead want to save to a file, use ``fig.savefig("daylight.png")``. +# +# +# Collect Axes properties into a single ``set()`` call +# ==================================================== +# +# The properties of Matplotlib Artists can be modified through their respective +# ``set_*()`` methods. Artists additionally have a generic ``set()`` method, that takes +# keyword arguments and is equivalent to calling all the respective ``set_*()`` methods. +# :: +# +# ax.set_xlabel("day") +# ax.set_ylabel("daylight hours") +# +# can also be written as :: +# +# ax.set(xlabel="day", ylabel="daylight hours") +# +# This is the most simple and effective reduction you can do. With that we can shorten +# the above plot to + +fig, ax = plt.subplots() +ax.plot(day, hours, color="orange") +ax.set(xlabel="day", ylabel="daylight hours", title="London") + +# %% +# +# This works as long as you only need to pass one parameter to the ``set_*()`` function. +# The individual functions are still necessary if you want more control, e.g. +# ``set_title("London", fontsize=16)``. +# +# +# Not storing a reference to the figure +# ===================================== +# Another nuisance of ``fig, ax = plt.subplots()`` is that you always create a ``fig`` +# variable, even if you don't use it. A slightly shorter version would be using the +# standard variable for unused value in Python (``_``): ``_, ax = plt.subplots()``. +# However, that's only marginally better. +# +# You can work around this by separating figure and Axes creation and chaining them :: +# +# ax = plt.figure().add_subplot() +# +# This is a bit cleaner logically and has the slight advantage that you could set +# figure properties inline as well; e.g. ``plt.figure(facecolor="lightgoldenrod")``. +# But it has the disadvantage that it's longer than ``fig, ax = plt.subplots()``. +# +# You can still obtain the figure from the Axes if needed, e.g. :: +# +# ax.figure.savefig("daylight_hours.png") +# +# The example code now looks like this: + +ax = plt.figure().add_subplot() +ax.plot(day, hours, color="orange") +ax.set(xlabel="day", ylabel="daylight hours", title="London") + +# %% +# Define Axes properties during axes creation +# =========================================== +# The ``set_*`` methods as well as ``set`` modify existing objects. You can +# alternatively define them right at creation. Since you typically don't instantiate +# classes yourself in Matplotlib, but rather call some factory function that creates +# the object and wires it up correctly with the plot, this may seem less obvious. But +# in fact you just pass the desired properties to the factory functions. You are likely +# doing this already in some places without realizing. Consider the function to create +# a line :: +# +# ax.plot(x, y, color="orange") +# +# This is equivalent to :: +# +# line, = ax.plot(x, y) +# line.set_color("orange") +# +# The same can be done with functions that create Axes. + +ax = plt.figure().add_subplot(xlabel="day", ylabel="daylight hours", title="London") +ax.plot(day, hours, color="orange") + +# %% +# .. important:: +# The Axes properties are only accepted as keyword arguments by +# `.Figure.add_subplot`, which creates a single Axes. +# +# For `.Figure.subplots` and `.pyplot.subplots`, you'd have to pass the properties +# as a dict via the keyword argument ``subplot_kw``. The limitation here is that +# such parameters are given to all Axes. For example, if you need two subplots +# (``fig, (ax1, ax2) = plt.subplots(1, 2)``) with different labels, you have to +# set them individually. +# +# Defining Axes properties during creation is best used for single subplots or when +# all subplots share the same properties. +# +# +# Using implicit figure creation +# ============================== +# You can go even further by tapping into the pyplot logic and use `.pyplot.axes` to +# create the axes: + +ax = plt.axes(xlabel="day", ylabel="daylight hours", title="London") +ax.plot(day, hours, color="orange") + +# %% +# .. warning:: +# When using this, you have to be aware of the implicit figure semantics of pyplot. +# ``plt.axes()`` will only create a new figure if no figure exists. Otherwise, it +# will add the Axes to the current existing figure, which is likely not what you +# want. +# +# Not storing a reference to the Axes +# =================================== +# If you only need to visualize one dataset, you can append the plot command +# directly to the Axes creation. This may be useful e.g. in notebooks, +# where you want to create a plot with some configuration, but as little distracting +# code as possible: + +plt.axes(xlabel="day", ylabel="daylight hours").plot(day, hours, color="orange") diff --git a/galleries/tutorials/index.rst b/galleries/tutorials/index.rst index 48187a862a2e..76c0037dca11 100644 --- a/galleries/tutorials/index.rst +++ b/galleries/tutorials/index.rst @@ -32,6 +32,23 @@ a :ref:`FAQ ` in our :ref:`user guide `.
+.. raw:: html + +
+ +.. only:: html + + .. image:: /tutorials/images/thumb/sphx_glr_coding_shortcuts_thumb.png + :alt: Coding shortcuts + + :ref:`sphx_glr_tutorials_coding_shortcuts.py` + +.. raw:: html + +
Coding shortcuts
+
+ + .. raw:: html
@@ -92,6 +109,7 @@ a :ref:`FAQ ` in our :ref:`user guide `. :hidden: /tutorials/pyplot + /tutorials/coding_shortcuts /tutorials/images /tutorials/lifecycle /tutorials/artists From 8c7d40fb2771abf78e9a1391c938e840c65aead0 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:34:01 +0000 Subject: [PATCH 10/30] Backport PR #30969: DOC: Simplify barh() example --- galleries/examples/lines_bars_and_markers/barh.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/galleries/examples/lines_bars_and_markers/barh.py b/galleries/examples/lines_bars_and_markers/barh.py index 5493c7456c75..c8834e252cb6 100644 --- a/galleries/examples/lines_bars_and_markers/barh.py +++ b/galleries/examples/lines_bars_and_markers/barh.py @@ -15,12 +15,10 @@ # Example data people = ('Tom', 'Dick', 'Harry', 'Slim', 'Jim') -y_pos = np.arange(len(people)) performance = 3 + 10 * np.random.rand(len(people)) error = np.random.rand(len(people)) -ax.barh(y_pos, performance, xerr=error, align='center') -ax.set_yticks(y_pos, labels=people) +ax.barh(people, performance, xerr=error, align='center') ax.invert_yaxis() # labels read top-to-bottom ax.set_xlabel('Performance') ax.set_title('How fast do you want to go today?') From fe5ca0610b2ef745bf4af8021f92911b53071c00 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:34:01 +0000 Subject: [PATCH 11/30] Backport PR #30969: DOC: Simplify barh() example --- galleries/examples/lines_bars_and_markers/barh.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/galleries/examples/lines_bars_and_markers/barh.py b/galleries/examples/lines_bars_and_markers/barh.py index 5493c7456c75..c8834e252cb6 100644 --- a/galleries/examples/lines_bars_and_markers/barh.py +++ b/galleries/examples/lines_bars_and_markers/barh.py @@ -15,12 +15,10 @@ # Example data people = ('Tom', 'Dick', 'Harry', 'Slim', 'Jim') -y_pos = np.arange(len(people)) performance = 3 + 10 * np.random.rand(len(people)) error = np.random.rand(len(people)) -ax.barh(y_pos, performance, xerr=error, align='center') -ax.set_yticks(y_pos, labels=people) +ax.barh(people, performance, xerr=error, align='center') ax.invert_yaxis() # labels read top-to-bottom ax.set_xlabel('Performance') ax.set_title('How fast do you want to go today?') From 89a35d8aaf4c7c4d15f0cc0876293f4c6156bbd9 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh <14363975+scottshambaugh@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:01:03 -0700 Subject: [PATCH 12/30] Backport PR #30985: MNT: do not assign a numpy array shape --- lib/matplotlib/tests/test_axes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 159fc70282b8..b9eb145b1410 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2182,9 +2182,8 @@ def test_pcolor_regression(pd): time_axis, y_axis = np.meshgrid(times, y_vals) shape = (len(y_vals) - 1, len(times) - 1) - z_data = np.arange(shape[0] * shape[1]) + z_data = np.arange(shape[0] * shape[1]).reshape(shape) - z_data.shape = shape try: register_matplotlib_converters() From 2deb54c8d78ba071ba4042af1be81bea79b6a164 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:37:14 +0000 Subject: [PATCH 13/30] Backport PR #31035: DOCS: Fix typo in time array step size comment --- galleries/examples/animation/double_pendulum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galleries/examples/animation/double_pendulum.py b/galleries/examples/animation/double_pendulum.py index 7a42a6d989ba..76076341d5c2 100644 --- a/galleries/examples/animation/double_pendulum.py +++ b/galleries/examples/animation/double_pendulum.py @@ -51,7 +51,7 @@ def derivs(t, state): return dydx -# create a time array from 0..t_stop sampled at 0.02 second steps +# create a time array from 0..t_stop sampled at 0.01 second steps dt = 0.01 t = np.arange(0, t_stop, dt) From 3039ff1b3baa223f5b4d59412829b7f33f9271d1 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:37:14 +0000 Subject: [PATCH 14/30] Backport PR #31035: DOCS: Fix typo in time array step size comment --- galleries/examples/animation/double_pendulum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galleries/examples/animation/double_pendulum.py b/galleries/examples/animation/double_pendulum.py index 7a42a6d989ba..76076341d5c2 100644 --- a/galleries/examples/animation/double_pendulum.py +++ b/galleries/examples/animation/double_pendulum.py @@ -51,7 +51,7 @@ def derivs(t, state): return dydx -# create a time array from 0..t_stop sampled at 0.02 second steps +# create a time array from 0..t_stop sampled at 0.01 second steps dt = 0.01 t = np.arange(0, t_stop, dt) From 313a44d28fea53da5db65a69d41b7009af91a780 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 4 Feb 2026 15:41:41 -0500 Subject: [PATCH 15/30] Backport PR #31075: Fix remove method for figure title and xy-labels --- lib/matplotlib/figure.py | 7 +++++++ lib/matplotlib/tests/test_figure.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 089141727189..e5794954abb8 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -376,10 +376,17 @@ def _suplabels(self, t, info, **kwargs): else: suplab = self.text(x, y, t, **kwargs) setattr(self, info['name'], suplab) + suplab._remove_method = functools.partial(self._remove_suplabel, + name=info['name']) + suplab._autopos = autopos self.stale = True return suplab + def _remove_suplabel(self, label, name): + self.texts.remove(label) + setattr(self, name, None) + @_docstring.Substitution(x0=0.5, y0=0.98, name='super title', ha='center', va='top', rc='title') @_docstring.copy(_suplabels) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 3a4ced254091..d24f701b4085 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -364,6 +364,28 @@ def test_get_suptitle_supxlabel_supylabel(): assert fig.get_supylabel() == 'supylabel' +def test_remove_suptitle_supxlabel_supylabel(): + fig = plt.figure() + + title = fig.suptitle('suptitle') + xlabel = fig.supxlabel('supxlabel') + ylabel = fig.supylabel('supylabel') + + assert len(fig.texts) == 3 + assert fig._suptitle is not None + assert fig._supxlabel is not None + assert fig._supylabel is not None + + title.remove() + assert fig._suptitle is None + xlabel.remove() + assert fig._supxlabel is None + ylabel.remove() + assert fig._supylabel is None + + assert not fig.texts + + @image_comparison(['alpha_background'], # only test png and svg. The PDF output appears correct, # but Ghostscript does not preserve the background color. From b9d7e7f73eee09ebd33e9c8505c5d0c4d8866dcf Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 13 Feb 2026 13:22:13 -0600 Subject: [PATCH 16/30] Backport PR #31153: TST: Use correct method of clearing mock objects --- lib/matplotlib/tests/test_backends_interactive.py | 2 +- lib/matplotlib/tests/test_colors.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 4e3c1bbc2bb5..594681391e61 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -648,7 +648,7 @@ def _impl_test_interactive_timers(): assert mock.call_count > 1 # Now turn it into a single shot timer and verify only one gets triggered - mock.call_count = 0 + mock.reset_mock() timer.single_shot = True timer.start() plt.pause(pause_time) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 7da4bab69c15..7ca9c0f77858 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1590,7 +1590,7 @@ def test_norm_callback(): assert increment.call_count == 2 # We only want autoscale() calls to send out one update signal - increment.call_count = 0 + increment.reset_mock() norm.autoscale([0, 1, 2]) assert increment.call_count == 1 From ef19bdcc9a1b199c00d9948cf73044b9cdfea435 Mon Sep 17 00:00:00 2001 From: Aman Srivastava <160766756+aman-coder03@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:04:28 +0530 Subject: [PATCH 17/30] Backport PR #31278: Fix `clabel` manual argument not accepting unit-typed coordinates --- lib/matplotlib/contour.py | 2 ++ lib/matplotlib/tests/test_datetime.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index f7318d578121..4f3e594e9202 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -446,6 +446,8 @@ def add_label_near(self, x, y, inline=True, inline_spacing=5, if transform is None: transform = self.axes.transData if transform: + x = self.axes.convert_xunits(x) + y = self.axes.convert_yunits(y) x, y = transform.transform((x, y)) idx_level_min, idx_vtx_min, proj = self._find_nearest_contour( diff --git a/lib/matplotlib/tests/test_datetime.py b/lib/matplotlib/tests/test_datetime.py index 276056d044ae..9fc133549017 100644 --- a/lib/matplotlib/tests/test_datetime.py +++ b/lib/matplotlib/tests/test_datetime.py @@ -259,11 +259,22 @@ def test_bxp(self): ax.xaxis.set_major_formatter(mpl.dates.DateFormatter("%Y-%m-%d")) ax.set_title('Box plot with datetime data') - @pytest.mark.xfail(reason="Test for clabel not written yet") @mpl.style.context("default") def test_clabel(self): + dates = [datetime.datetime(2023, 10, 1) + datetime.timedelta(days=i) + for i in range(10)] + x = np.arange(-10.0, 5.0, 0.5) + X, Y = np.meshgrid(x, dates) + Z = np.arange(X.size).reshape(X.shape) + fig, ax = plt.subplots() - ax.clabel(...) + CS = ax.contour(X, Y, Z) + labels = ax.clabel(CS, manual=[(x[0], dates[0])]) + assert len(labels) == 1 + assert labels[0].get_text() == '0' + x_pos, y_pos = labels[0].get_position() + assert x_pos == pytest.approx(-10.0, abs=1e-3) + assert y_pos == pytest.approx(mpl.dates.date2num(dates[0]), abs=1e-3) @mpl.style.context("default") def test_contour(self): From f32af1d8e71ee524453155c0265a46f913028860 Mon Sep 17 00:00:00 2001 From: Vikash Kumar <163628932+Vikash-Kumar-23@users.noreply.github.com> Date: Sat, 28 Mar 2026 03:38:39 +0530 Subject: [PATCH 18/30] Backport PR #31323: FIX: Prevent crash when removing a subfigure containing subplots --- lib/matplotlib/artist.py | 8 +++++--- lib/matplotlib/tests/test_figure.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index c87c789048c4..25515da77ed7 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -240,10 +240,12 @@ def remove(self): # clear stale callback self.stale_callback = None _ax_flag = False - if hasattr(self, 'axes') and self.axes: + ax = getattr(self, 'axes', None) + mouseover_set = getattr(ax, '_mouseover_set', None) + if mouseover_set is not None: # remove from the mouse hit list - self.axes._mouseover_set.discard(self) - self.axes.stale = True + mouseover_set.discard(self) + ax.stale = True self.axes = None # decouple the artist from the Axes _ax_flag = True diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 3a4ced254091..7fb3598e84bb 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -1551,6 +1551,7 @@ def test_subfigures_wspace_hspace(): def test_subfigure_remove(): fig = plt.figure() sfs = fig.subfigures(2, 2) + sfs[1, 1].subplots() sfs[1, 1].remove() assert len(fig.subfigs) == 3 From 6aac3dad99575575744972f7b68c80be28ec736a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 27 Mar 2026 01:43:56 -0400 Subject: [PATCH 19/30] Backport PR #31401: BLD: Temporarily pin setuptools-scm<10 BLD: Temporarily pin setuptools-scm<10 (cherry picked from commit 443c728e3af423a882d51e758c88577959e0c7d1) --- .github/workflows/cygwin.yml | 2 +- .github/workflows/tests.yml | 2 +- environment.yml | 2 +- pyproject.toml | 4 ++-- requirements/dev/build-requirements.txt | 2 +- requirements/testing/mypy.txt | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index bde902013412..befc83162075 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -181,7 +181,7 @@ jobs: export PATH="/usr/local/bin:$PATH" python -m pip install --no-build-isolation 'contourpy>=1.0.1' python -m pip install --upgrade cycler fonttools \ - packaging pyparsing python-dateutil setuptools-scm \ + packaging pyparsing python-dateutil 'setuptools-scm<10' \ -r requirements_test.txt sphinx ipython python -m pip install --upgrade pycairo 'cairocffi>=0.8' PyGObject && python -c 'import gi; gi.require_version("Gtk", "3.0"); from gi.repository import Gtk' && diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ea402d954137..906c6c4408d7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -243,7 +243,7 @@ jobs: # Preinstall build requirements to enable no-build-isolation builds. python -m pip install --upgrade $PRE \ 'contourpy>=1.0.1' cycler fonttools kiwisolver importlib_resources \ - packaging pillow 'pyparsing!=3.1.0' python-dateutil setuptools-scm \ + packaging pillow 'pyparsing!=3.1.0' python-dateutil 'setuptools-scm<10' \ 'meson-python>=0.13.1' 'pybind11>=2.13.2' \ -r requirements/testing/all.txt \ ${{ matrix.extra-requirements }} diff --git a/environment.yml b/environment.yml index 2a4f3eff69ea..f39ba3f93f2f 100644 --- a/environment.yml +++ b/environment.yml @@ -26,7 +26,7 @@ dependencies: - pyqt - python>=3.10 - python-dateutil>=2.1 - - setuptools_scm + - setuptools_scm<10 - wxpython # building documentation - colorspacious diff --git a/pyproject.toml b/pyproject.toml index 23c441b52c9c..96b69829e674 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ requires-python = ">=3.10" dev = [ "meson-python>=0.13.1,<0.17.0", "pybind11>=2.13.2,!=2.13.3", - "setuptools_scm>=7", + "setuptools_scm>=7,<10", # Not required by us but setuptools_scm without a version, cso _if_ # installed, then setuptools_scm 8 requires at least this version. # Unfortunately, we can't do a sort of minimum-if-instaled dependency, so @@ -74,7 +74,7 @@ build-backend = "mesonpy" requires = [ "meson-python>=0.13.1,<0.17.0", "pybind11>=2.13.2,!=2.13.3", - "setuptools_scm>=7", + "setuptools_scm>=7,<10", ] [tool.meson-python.args] diff --git a/requirements/dev/build-requirements.txt b/requirements/dev/build-requirements.txt index 4d2a098c3c4f..372a7d669fb1 100644 --- a/requirements/dev/build-requirements.txt +++ b/requirements/dev/build-requirements.txt @@ -1,3 +1,3 @@ pybind11>=2.13.2,!=2.13.3 meson-python -setuptools-scm +setuptools-scm<10 diff --git a/requirements/testing/mypy.txt b/requirements/testing/mypy.txt index 0cef979a34bf..4421560494d7 100644 --- a/requirements/testing/mypy.txt +++ b/requirements/testing/mypy.txt @@ -22,5 +22,5 @@ packaging>=20.0 pillow>=8 pyparsing>=3 python-dateutil>=2.7 -setuptools_scm>=7 +setuptools_scm>=7,<10 setuptools>=64 From 3c91aec2b20adf547b102c68b37cfdedb03e7d54 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:51:07 +0200 Subject: [PATCH 20/30] Backport PR #31420: Fix outdated Savannah URL for freetype download --- subprojects/freetype-2.6.1.wrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subprojects/freetype-2.6.1.wrap b/subprojects/freetype-2.6.1.wrap index 763362b84df0..270556f0d5d3 100644 --- a/subprojects/freetype-2.6.1.wrap +++ b/subprojects/freetype-2.6.1.wrap @@ -1,5 +1,5 @@ [wrap-file] -source_url = https://download.savannah.gnu.org/releases/freetype/freetype-old/freetype-2.6.1.tar.gz +source_url = https://download.savannah.nongnu.org/releases/freetype/freetype-old/freetype-2.6.1.tar.gz source_fallback_url = https://downloads.sourceforge.net/project/freetype/freetype2/2.6.1/freetype-2.6.1.tar.gz source_filename = freetype-2.6.1.tar.gz source_hash = 0a3c7dfbda6da1e8fce29232e8e96d987ababbbf71ebc8c75659e4132c367014 From 69e6954018d082dd2c396b312b4ce40922176092 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 2 Apr 2026 15:28:06 -0400 Subject: [PATCH 21/30] Backport PR #31437: mathtext: Fix type inconsistency with fontmaps mathtext: Fix type inconsistency with fontmaps (cherry picked from commit fc6aa040a6e5d4a5bc6557f257f9102edd447b4b) --- ci/mypy-stubtest-allowlist.txt | 1 - lib/matplotlib/_mathtext.py | 73 +++++++++++++++------------------- lib/matplotlib/ft2font.pyi | 4 +- 3 files changed, 35 insertions(+), 43 deletions(-) diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 46ec06e0a9f1..85d9be15673e 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -28,7 +28,6 @@ matplotlib\.ticker\.LogitLocator\.nonsingular # Stdlib/Enum considered inconsistent (no fault of ours, I don't think) matplotlib\.backend_bases\._Mode\.__new__ -matplotlib\.units\.Number\.__hash__ # 3.6 Pending deprecations matplotlib\.figure\.Figure\.set_constrained_layout diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index cf35dc1de7db..50077c6738fa 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -331,20 +331,15 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag # Per-instance cache. self._get_info = functools.cache(self._get_info) # type: ignore[method-assign] self._fonts = {} - self.fontmap: dict[str | int, str] = {} + self.fontmap: dict[str, str] = {} filename = findfont(self.default_font_prop) default_font = get_font(filename) self._fonts['default'] = default_font self._fonts['regular'] = default_font - def _get_font(self, font: str | int) -> FT2Font: - if font in self.fontmap: - basename = self.fontmap[font] - else: - # NOTE: An int is only passed by subclasses which have placed int keys into - # `self.fontmap`, so we must cast this to confirm it to typing. - basename = T.cast(str, font) + def _get_font(self, font: str) -> FT2Font: + basename = self.fontmap.get(font, font) cached_font = self._fonts.get(basename) if cached_font is None and os.path.exists(basename): cached_font = get_font(basename) @@ -574,12 +569,13 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag # include STIX sized alternatives for glyphs if fallback is STIX if isinstance(self._fallback_font, StixFonts): stixsizedaltfonts = { - 0: 'STIXGeneral', - 1: 'STIXSizeOneSym', - 2: 'STIXSizeTwoSym', - 3: 'STIXSizeThreeSym', - 4: 'STIXSizeFourSym', - 5: 'STIXSizeFiveSym'} + '0': 'STIXGeneral', + '1': 'STIXSizeOneSym', + '2': 'STIXSizeTwoSym', + '3': 'STIXSizeThreeSym', + '4': 'STIXSizeFourSym', + '5': 'STIXSizeFiveSym', + } for size, name in stixsizedaltfonts.items(): fullpath = findfont(name) @@ -637,7 +633,7 @@ def _get_glyph(self, fontname: str, font_class: str, g = self._fallback_font._get_glyph(fontname, font_class, sym) family = g[0].family_name - if family in list(BakomaFonts._fontmap.values()): + if family in BakomaFonts._fontmap.values(): family = "Computer Modern" _log.info("Substituting symbol %s from %s", sym, family) return g @@ -658,13 +654,12 @@ def _get_glyph(self, fontname: str, font_class: str, def get_sized_alternatives_for_symbol(self, fontname: str, sym: str) -> list[tuple[str, str]]: if self._fallback_font: - return self._fallback_font.get_sized_alternatives_for_symbol( - fontname, sym) + return self._fallback_font.get_sized_alternatives_for_symbol(fontname, sym) return [(fontname, sym)] class DejaVuFonts(UnicodeFonts, metaclass=abc.ABCMeta): - _fontmap: dict[str | int, str] = {} + _fontmap: dict[str, str] = {} def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlags): # This must come first so the backend's owner is set correctly @@ -676,11 +671,11 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag TruetypeFonts.__init__(self, default_font_prop, load_glyph_flags) # Include Stix sized alternatives for glyphs self._fontmap.update({ - 1: 'STIXSizeOneSym', - 2: 'STIXSizeTwoSym', - 3: 'STIXSizeThreeSym', - 4: 'STIXSizeFourSym', - 5: 'STIXSizeFiveSym', + '1': 'STIXSizeOneSym', + '2': 'STIXSizeTwoSym', + '3': 'STIXSizeThreeSym', + '4': 'STIXSizeFourSym', + '5': 'STIXSizeFiveSym', }) for key, name in self._fontmap.items(): fullpath = findfont(name) @@ -718,7 +713,7 @@ class DejaVuSerifFonts(DejaVuFonts): 'sf': 'DejaVu Sans', 'tt': 'DejaVu Sans Mono', 'ex': 'DejaVu Serif Display', - 0: 'DejaVu Serif', + '0': 'DejaVu Serif', } @@ -736,7 +731,7 @@ class DejaVuSansFonts(DejaVuFonts): 'sf': 'DejaVu Sans', 'tt': 'DejaVu Sans Mono', 'ex': 'DejaVu Sans Display', - 0: 'DejaVu Sans', + '0': 'DejaVu Sans', } @@ -752,7 +747,7 @@ class StixFonts(UnicodeFonts): - handles sized alternative characters for the STIXSizeX fonts. """ - _fontmap: dict[str | int, str] = { + _fontmap = { 'rm': 'STIXGeneral', 'it': 'STIXGeneral:italic', 'bf': 'STIXGeneral:weight=bold', @@ -760,12 +755,12 @@ class StixFonts(UnicodeFonts): 'nonunirm': 'STIXNonUnicode', 'nonuniit': 'STIXNonUnicode:italic', 'nonunibf': 'STIXNonUnicode:weight=bold', - 0: 'STIXGeneral', - 1: 'STIXSizeOneSym', - 2: 'STIXSizeTwoSym', - 3: 'STIXSizeThreeSym', - 4: 'STIXSizeFourSym', - 5: 'STIXSizeFiveSym', + '0': 'STIXGeneral', + '1': 'STIXSizeOneSym', + '2': 'STIXSizeTwoSym', + '3': 'STIXSizeThreeSym', + '4': 'STIXSizeFourSym', + '5': 'STIXSizeFiveSym', } _fallback_font = None _sans = False @@ -832,10 +827,8 @@ def _map_virtual_font(self, fontname: str, font_class: str, return fontname, uniindex @functools.cache - def get_sized_alternatives_for_symbol( # type: ignore[override] - self, - fontname: str, - sym: str) -> list[tuple[str, str]] | list[tuple[int, str]]: + def get_sized_alternatives_for_symbol(self, fontname: str, + sym: str) -> list[tuple[str, str]]: fixes = { '\\{': '{', '\\}': '}', '\\[': '[', '\\]': ']', '<': '\N{MATHEMATICAL LEFT ANGLE BRACKET}', @@ -846,8 +839,8 @@ def get_sized_alternatives_for_symbol( # type: ignore[override] uniindex = get_unicode_index(sym) except ValueError: return [(fontname, sym)] - alternatives = [(i, chr(uniindex)) for i in range(6) - if self._get_font(i).get_char_index(uniindex) != 0] + alternatives = [(str(i), chr(uniindex)) for i in range(6) + if self._get_font(str(i)).get_char_index(uniindex) != 0] # The largest size of the radical symbol in STIX has incorrect # metrics that cause it to be disconnected from the stem. if sym == r'\__sqrt__': @@ -1542,7 +1535,7 @@ def __init__(self, c: str, height: float, depth: float, state: ParserState, break shift = 0.0 - if state.font != 0 or len(alternatives) == 1: + if state.font != '0' or len(alternatives) == 1: if factor is None: factor = target_total / (char.height + char.depth) state.fontsize *= factor @@ -2530,7 +2523,7 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: # Handle regular sub/superscripts constants = _get_font_constant_set(state) lc_height = last_char.height - lc_baseline = 0 + lc_baseline = 0.0 if self.is_dropsub(last_char): lc_baseline = last_char.depth diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 37281afeaafa..3c8b52a73b6b 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -194,7 +194,7 @@ class FT2Font(Buffer): _kerning_factor: int = ... ) -> None: ... if sys.version_info[:2] >= (3, 12): - def __buffer__(self, flags: int) -> memoryview: ... + def __buffer__(self, /, flags: int) -> memoryview: ... def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ... def clear(self) -> None: ... def draw_glyph_to_bitmap( @@ -286,7 +286,7 @@ class FT2Image(Buffer): def __init__(self, width: int, height: int) -> None: ... def draw_rect_filled(self, x0: int, y0: int, x1: int, y1: int) -> None: ... if sys.version_info[:2] >= (3, 12): - def __buffer__(self, flags: int) -> memoryview: ... + def __buffer__(self, /, flags: int) -> memoryview: ... @final class Glyph: From cada119f034458b6335bc7b9463618352d3ce89b Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 3 Apr 2026 16:05:56 -0500 Subject: [PATCH 22/30] Bump mimimum Ubuntu Version on Azure because 20.04 is EOL --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cfd4f65dc775..5dce09e6d36b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -50,7 +50,7 @@ stages: strategy: matrix: Linux_py310: - vmImage: 'ubuntu-20.04' # keep one job pinned to the oldest image + vmImage: 'ubuntu-22.04' # keep one job pinned to the oldest image python.version: '3.10' Linux_py311: vmImage: 'ubuntu-latest' From 614abdd5e38ad6cde58c5aaf6c2ca229cd29edab Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 16 Apr 2026 16:27:00 -0400 Subject: [PATCH 23/30] Backport PR #31504: Re-order variants to prioritize narrower types --- src/_backend_agg_wrapper.cpp | 4 ++-- src/ft2font_wrapper.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 269e2aaa9ee5..80bb14e8cfec 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -58,8 +58,8 @@ PyRendererAgg_draw_path(RendererAgg *self, static void PyRendererAgg_draw_text_image(RendererAgg *self, py::array_t image_obj, - std::variant vx, - std::variant vy, + std::variant vx, + std::variant vy, double angle, GCAgg &gc) { diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 9b54721810d6..3008c0b08b2f 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -14,7 +14,7 @@ namespace py = pybind11; using namespace pybind11::literals; template -using double_or_ = std::variant; +using double_or_ = std::variant; template static T From 2da812c083736d5109b4f27b3b4b524562d66377 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 24 Jan 2026 09:34:07 +0100 Subject: [PATCH 24/30] Backport PR #31020: DOC: Fix doc builds with Sphinx 9 --- doc/conf.py | 1 + lib/matplotlib/backend_tools.py | 18 ++++++++++++------ lib/matplotlib/backend_tools.pyi | 6 +++--- lib/matplotlib/widgets.py | 4 ++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 292aff3e1983..45fe32f79f88 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -891,3 +891,4 @@ def setup(app): if sphinx.version_info[:2] < (7, 1): app.connect('html-page-context', add_html_cache_busting, priority=1000) generate_ScalarMappable_docs() + app.config.autodoc_use_legacy_class_based = True diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 87ed794022a0..784f97c0a1e4 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -937,6 +937,7 @@ def trigger(self, *args, **kwargs): self.toolmanager.message_event(message, self) +#: The default tools to add to a tool manager. default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward, 'zoom': ToolZoom, 'pan': ToolPan, 'subplots': ConfigureSubplotsBase, @@ -956,12 +957,13 @@ def trigger(self, *args, **kwargs): 'copy': ToolCopyToClipboardBase, } +#: The default tools to add to a container. default_toolbar_tools = [['navigation', ['home', 'back', 'forward']], ['zoompan', ['pan', 'zoom', 'subplots']], ['io', ['save', 'help']]] -def add_tools_to_manager(toolmanager, tools=default_tools): +def add_tools_to_manager(toolmanager, tools=None): """ Add multiple tools to a `.ToolManager`. @@ -971,14 +973,16 @@ def add_tools_to_manager(toolmanager, tools=default_tools): Manager to which the tools are added. tools : {str: class_like}, optional The tools to add in a {name: tool} dict, see - `.backend_managers.ToolManager.add_tool` for more info. + `.backend_managers.ToolManager.add_tool` for more info. If not specified, then + defaults to `.default_tools`. """ - + if tools is None: + tools = default_tools for name, tool in tools.items(): toolmanager.add_tool(name, tool) -def add_tools_to_container(container, tools=default_toolbar_tools): +def add_tools_to_container(container, tools=None): """ Add multiple tools to the container. @@ -990,9 +994,11 @@ def add_tools_to_container(container, tools=default_toolbar_tools): tools : list, optional List in the form ``[[group1, [tool1, tool2 ...]], [group2, [...]]]`` where the tools ``[tool1, tool2, ...]`` will display in group1. - See `.backend_bases.ToolContainerBase.add_tool` for details. + See `.backend_bases.ToolContainerBase.add_tool` for details. If not specified, + then defaults to `.default_toolbar_tools`. """ - + if tools is None: + tools = default_toolbar_tools for group, grouptools in tools: for position, tool in enumerate(grouptools): container.add_tool(tool, group, position) diff --git a/lib/matplotlib/backend_tools.pyi b/lib/matplotlib/backend_tools.pyi index 32fe8c2f5a79..fa89e3d66871 100644 --- a/lib/matplotlib/backend_tools.pyi +++ b/lib/matplotlib/backend_tools.pyi @@ -112,10 +112,10 @@ class ToolHelpBase(ToolBase): class ToolCopyToClipboardBase(ToolBase): ... -default_tools: dict[str, ToolBase] +default_tools: dict[str, type[ToolBase]] default_toolbar_tools: list[list[str | list[str]]] def add_tools_to_manager( - toolmanager: ToolManager, tools: dict[str, type[ToolBase]] = ... + toolmanager: ToolManager, tools: dict[str, type[ToolBase]] | None = ... ) -> None: ... -def add_tools_to_container(container: ToolContainerBase, tools: list[Any] = ...) -> None: ... +def add_tools_to_container(container: ToolContainerBase, tools: list[Any] | None = ...) -> None: ... diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 6a3c0d684380..039d7133ded1 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -980,7 +980,7 @@ class CheckButtons(AxesWidget): For the check buttons to remain responsive you must keep a reference to this object. - Connect to the CheckButtons with the `.on_clicked` method. + Connect to the CheckButtons with the `~._Buttons.on_clicked` method. Attributes ---------- @@ -1545,7 +1545,7 @@ class RadioButtons(AxesWidget): For the buttons to remain responsive you must keep a reference to this object. - Connect to the RadioButtons with the `.on_clicked` method. + Connect to the RadioButtons with the `~._Buttons.on_clicked` method. Attributes ---------- From acc60241a70b920eaf04fce41a8cf0a77010fb7d Mon Sep 17 00:00:00 2001 From: Benjamin Root Date: Fri, 13 Mar 2026 09:07:57 -0400 Subject: [PATCH 25/30] Merge pull request #31282 from scottshambaugh/tex_no_shell SEC: Block shell escapes in latex and ps commands (cherry picked from commit 8ff895d0750f3b16c3214b38a91ad78029c82df7) The test that was edited had significant updates on main, so the old test was kept on backport and no similar call exists in the old test. --- lib/matplotlib/backends/backend_pgf.py | 6 +++--- lib/matplotlib/backends/backend_ps.py | 6 ++++-- lib/matplotlib/testing/__init__.py | 3 ++- lib/matplotlib/texmanager.py | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 48b6e8ac152c..2d2e24c3286c 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -281,7 +281,7 @@ def _setup_latex_process(self, *, expect_reply=True): # it. try: self.latex = subprocess.Popen( - [mpl.rcParams["pgf.texsystem"], "-halt-on-error"], + [mpl.rcParams["pgf.texsystem"], "-halt-on-error", "-no-shell-escape"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, encoding="utf-8", cwd=self.tmpdir) except FileNotFoundError as err: @@ -848,7 +848,7 @@ def print_pdf(self, fname_or_fh, *, metadata=None, **kwargs): texcommand = mpl.rcParams["pgf.texsystem"] cbook._check_and_log_subprocess( [texcommand, "-interaction=nonstopmode", "-halt-on-error", - "figure.tex"], _log, cwd=tmpdir) + "-no-shell-escape", "figure.tex"], _log, cwd=tmpdir) with ((tmppath / "figure.pdf").open("rb") as orig, cbook.open_file_cm(fname_or_fh, "wb") as dest): shutil.copyfileobj(orig, dest) # copy file contents to target @@ -965,7 +965,7 @@ def _run_latex(self): tex_source.write_bytes(self._file.getvalue()) cbook._check_and_log_subprocess( [texcommand, "-interaction=nonstopmode", "-halt-on-error", - tex_source], + "-no-shell-escape", tex_source], _log, cwd=tmpdir) shutil.move(tex_source.with_suffix(".pdf"), self._output_name) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index f1f914ae5420..4dfdb2a6a095 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -1257,8 +1257,9 @@ def _convert_psfrags(tmppath, psfrags, paper_width, paper_height, orientation): with TemporaryDirectory() as tmpdir: psfile = os.path.join(tmpdir, "tmp.ps") + # -R1 is a security flag used to prevent shell command execution cbook._check_and_log_subprocess( - ['dvips', '-q', '-R0', '-o', psfile, dvifile], _log) + ['dvips', '-q', '-R1', '-o', psfile, dvifile], _log) shutil.move(psfile, tmppath) # check if the dvips created a ps in landscape paper. Somehow, @@ -1302,7 +1303,7 @@ def gs_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False): cbook._check_and_log_subprocess( [mpl._get_executable_info("gs").executable, - "-dBATCH", "-dNOPAUSE", "-r%d" % dpi, "-sDEVICE=ps2write", + "-dBATCH", "-dNOPAUSE", "-dSAFER", "-r%d" % dpi, "-sDEVICE=ps2write", *paper_option, f"-sOutputFile={psfile}", tmpfile], _log) @@ -1346,6 +1347,7 @@ def xpdf_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False): # happy (https://ghostscript.com/doc/9.56.1/Use.htm#MS_Windows). cbook._check_and_log_subprocess( ["ps2pdf", + "-dSAFER", "-dAutoFilterColorImages#false", "-dAutoFilterGrayImages#false", "-sAutoRotatePages#None", diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 19113d399626..73b1645468a6 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -164,7 +164,8 @@ def _check_for_pgf(texsystem): """, encoding="utf-8") try: subprocess.check_call( - [texsystem, "-halt-on-error", str(tex_path)], cwd=tmpdir, + [texsystem, "-halt-on-error", "-no-shell-escape", + str(tex_path)], cwd=tmpdir, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except (OSError, subprocess.CalledProcessError): return False diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 0424aede16eb..6856329931ef 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -291,8 +291,8 @@ def make_dvi(cls, tex, fontsize): Path(tmpdir, "file.tex").write_text( cls._get_tex_source(tex, fontsize), encoding='utf-8') cls._run_checked_subprocess( - ["latex", "-interaction=nonstopmode", "--halt-on-error", - "file.tex"], tex, cwd=tmpdir) + ["latex", "-interaction=nonstopmode", "-halt-on-error", + "-no-shell-escape", "file.tex"], tex, cwd=tmpdir) Path(tmpdir, "file.dvi").replace(dvifile) # Also move the tex source to the main cache directory, but # only for backcompat. From b2ed1969191a03ec8927f96573664474662ab4c1 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh <14363975+scottshambaugh@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:09:00 -0600 Subject: [PATCH 26/30] Backport PR #31248: SEC: Remove eval() from validate_cycler --- .../deprecations/31248-SS.rst | 9 ++ .../cycler_rcparam_security.rst | 14 ++++ lib/matplotlib/rcsetup.py | 83 ++++++++++++++----- lib/matplotlib/tests/test_rcparams.py | 24 ++++-- 4 files changed, 100 insertions(+), 30 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/31248-SS.rst create mode 100644 doc/users/next_whats_new/cycler_rcparam_security.rst diff --git a/doc/api/next_api_changes/deprecations/31248-SS.rst b/doc/api/next_api_changes/deprecations/31248-SS.rst new file mode 100644 index 000000000000..1d7adbdf0fde --- /dev/null +++ b/doc/api/next_api_changes/deprecations/31248-SS.rst @@ -0,0 +1,9 @@ +Arbitrary code in ``axes.prop_cycle`` rcParam strings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``axes.prop_cycle`` rcParam accepts Python expressions that are evaluated +in a limited context. The evaluation context has been further limited and some +expressions that previously worked (list comprehensions, for example) no longer +will. This change is made without a deprecation period to improve security. +The previously documented cycler operations at +https://matplotlib.org/cycler/ are still supported. diff --git a/doc/users/next_whats_new/cycler_rcparam_security.rst b/doc/users/next_whats_new/cycler_rcparam_security.rst new file mode 100644 index 000000000000..e4e2893aa994 --- /dev/null +++ b/doc/users/next_whats_new/cycler_rcparam_security.rst @@ -0,0 +1,14 @@ +``axes.prop_cycle`` rcParam security improvements +------------------------------------------------- + +The ``axes.prop_cycle`` rcParam is now parsed in a safer and more restricted +manner. Only literals, ``cycler()`` and ``concat()`` calls, the operators +``+`` and ``*``, and slicing are allowed. All previously valid cycler strings +documented at https://matplotlib.org/cycler/ are still supported, for example: + +.. code-block:: none + + axes.prop_cycle : cycler('color', ['r', 'g', 'b']) + cycler('linewidth', [1, 2, 3]) + axes.prop_cycle : 2 * cycler('color', 'rgb') + axes.prop_cycle : concat(cycler('color', 'rgb'), cycler('color', 'cmk')) + axes.prop_cycle : cycler('color', 'rgbcmk')[:3] diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index c23d9f818454..3a4da5f575fb 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -30,7 +30,7 @@ from matplotlib._enums import JoinStyle, CapStyle # Don't let the original cycler collide with our validating cycler -from cycler import Cycler, cycler as ccycler +from cycler import Cycler, concat as cconcat, cycler as ccycler @_api.caching_module_getattr @@ -759,11 +759,62 @@ def cycler(*args, **kwargs): return reduce(operator.add, (ccycler(k, v) for k, v in validated)) -class _DunderChecker(ast.NodeVisitor): - def visit_Attribute(self, node): - if node.attr.startswith("__") and node.attr.endswith("__"): - raise ValueError("cycler strings with dunders are forbidden") - self.generic_visit(node) +def _parse_cycler_string(s): + """ + Parse a string representation of a cycler into a Cycler object safely, + without using eval(). + + Accepts expressions like:: + + cycler('color', ['r', 'g', 'b']) + cycler('color', 'rgb') + cycler('linewidth', [1, 2, 3]) + cycler(c='rgb', lw=[1, 2, 3]) + cycler('c', 'rgb') * cycler('linestyle', ['-', '--']) + """ + try: + tree = ast.parse(s, mode='eval') + except SyntaxError as e: + raise ValueError(f"Could not parse {s!r}: {e}") from e + return _eval_cycler_expr(tree.body) + + +def _eval_cycler_expr(node): + """Recursively evaluate an AST node to build a Cycler object.""" + if isinstance(node, ast.BinOp): + left = _eval_cycler_expr(node.left) + right = _eval_cycler_expr(node.right) + if isinstance(node.op, ast.Add): + return left + right + if isinstance(node.op, ast.Mult): + return left * right + raise ValueError(f"Unsupported operator: {type(node.op).__name__}") + if isinstance(node, ast.Call): + if not (isinstance(node.func, ast.Name) + and node.func.id in ('cycler', 'concat')): + raise ValueError( + "only the 'cycler()' and 'concat()' functions are allowed") + func = cycler if node.func.id == 'cycler' else cconcat + args = [_eval_cycler_expr(a) for a in node.args] + kwargs = {kw.arg: _eval_cycler_expr(kw.value) for kw in node.keywords} + return func(*args, **kwargs) + if isinstance(node, ast.Subscript): + sl = node.slice + if not isinstance(sl, ast.Slice): + raise ValueError("only slicing is supported, not indexing") + s = slice( + ast.literal_eval(sl.lower) if sl.lower else None, + ast.literal_eval(sl.upper) if sl.upper else None, + ast.literal_eval(sl.step) if sl.step else None, + ) + value = _eval_cycler_expr(node.value) + return value[s] + # Allow literal values (int, strings, lists, tuples) as arguments + # to cycler() and concat(). + try: + return ast.literal_eval(node) + except (ValueError, TypeError): + raise ValueError( + f"Unsupported expression in cycler string: {ast.dump(node)}") # A validator dedicated to the named legend loc @@ -814,25 +865,11 @@ def _validate_legend_loc(loc): def validate_cycler(s): """Return a Cycler object from a string repr or the object itself.""" if isinstance(s, str): - # TODO: We might want to rethink this... - # While I think I have it quite locked down, it is execution of - # arbitrary code without sanitation. - # Combine this with the possibility that rcparams might come from the - # internet (future plans), this could be downright dangerous. - # I locked it down by only having the 'cycler()' function available. - # UPDATE: Partly plugging a security hole. - # I really should have read this: - # https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html - # We should replace this eval with a combo of PyParsing and - # ast.literal_eval() try: - _DunderChecker().visit(ast.parse(s)) - s = eval(s, {'cycler': cycler, '__builtins__': {}}) - except BaseException as e: + s = _parse_cycler_string(s) + except Exception as e: raise ValueError(f"{s!r} is not a valid cycler construction: {e}" ) from e - # Should make sure what comes from the above eval() - # is a Cycler object. if isinstance(s, Cycler): cycler_inst = s else: @@ -1101,7 +1138,7 @@ def _convert_validator_spec(key, conv): "axes.formatter.offset_threshold": validate_int, "axes.unicode_minus": validate_bool, # This entry can be either a cycler object or a string repr of a - # cycler-object, which gets eval()'ed to create the object. + # cycler-object, which is parsed safely via AST. "axes.prop_cycle": validate_cycler, # If "data", axes limits are set close to the data. # If "round_numbers" axes limits are set to the nearest round numbers. diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index bea5e90ea4e5..d800731ac53a 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -272,16 +272,23 @@ def generate_validator_testcases(valid): cycler('linestyle', ['-', '--'])), (cycler(mew=[2, 5]), cycler('markeredgewidth', [2, 5])), + ("2 * cycler('color', 'rgb')", 2 * cycler('color', 'rgb')), + ("2 * cycler('color', 'r' + 'gb')", 2 * cycler('color', 'rgb')), + ("cycler(c='r' + 'gb', lw=[1, 2, 3])", + cycler('color', 'rgb') + cycler('linewidth', [1, 2, 3])), + ("cycler('color', 'rgb') * 2", cycler('color', 'rgb') * 2), + ("concat(cycler('color', 'rgb'), cycler('color', 'cmk'))", + cycler('color', list('rgbcmk'))), + ("cycler('color', 'rgbcmk')[:3]", cycler('color', list('rgb'))), + ("cycler('color', 'rgb')[::-1]", cycler('color', list('bgr'))), ), - # This is *so* incredibly important: validate_cycler() eval's - # an arbitrary string! I think I have it locked down enough, - # and that is what this is testing. - # TODO: Note that these tests are actually insufficient, as it may - # be that they raised errors, but still did an action prior to - # raising the exception. We should devise some additional tests - # for that... + # validate_cycler() parses an arbitrary string using a safe + # AST-based parser (no eval). These tests verify that only valid + # cycler expressions are accepted. 'fail': ((4, ValueError), # Gotta be a string or Cycler object ('cycler("bleh, [])', ValueError), # syntax error + ("cycler('color', 'rgb') * * cycler('color', 'rgb')", # syntax error + ValueError), ('Cycler("linewidth", [1, 2, 3])', ValueError), # only 'cycler()' function is allowed # do not allow dunder in string literals @@ -295,6 +302,9 @@ def generate_validator_testcases(valid): ValueError), ("cycler('c', [j.__class__(j).lower() for j in ['r', 'b']])", ValueError), + # list comprehensions are arbitrary code, even if "safe" + ("cycler('color', [x for x in ['r', 'g', 'b']])", + ValueError), ('1 + 2', ValueError), # doesn't produce a Cycler object ('os.system("echo Gotcha")', ValueError), # os not available ('import os', ValueError), # should not be able to import From a83faacb0dbe7edd1bae38e1e715b77b6aaebb84 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 23 Apr 2026 15:39:56 -0500 Subject: [PATCH 27/30] Backport PR #31556: FIX: Inverted PyErr_Occurred check in enum type caster (_enums.h) --- src/_enums.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_enums.h b/src/_enums.h index 18f3d9aac9fa..e607b93f50f2 100644 --- a/src/_enums.h +++ b/src/_enums.h @@ -80,7 +80,7 @@ namespace p11x { auto ival = PyLong_AsLong(tmp); \ value = decltype(value)(ival); \ Py_DECREF(tmp); \ - return !(ival == -1 && !PyErr_Occurred()); \ + return !(ival == -1 && PyErr_Occurred()); \ } else { \ return false; \ } \ From 2fb18915bcfe69a188832c776fe18d88337de9bc Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 23 Apr 2026 17:46:08 -0500 Subject: [PATCH 28/30] REL: Release prep v3.10.9 --- doc/_static/switcher.json | 2 +- .../api_changes_3.10.9.rst} | 11 +++ doc/users/github_stats.rst | 90 ++++++++++++------- .../cycler_rcparam_security.rst | 14 --- .../prev_whats_new/github_stats_3.10.8.rst | 77 ++++++++++++++++ doc/users/release_notes.rst | 2 + 6 files changed, 149 insertions(+), 47 deletions(-) rename doc/api/{next_api_changes/deprecations/31248-SS.rst => prev_api_changes/api_changes_3.10.9.rst} (71%) delete mode 100644 doc/users/next_whats_new/cycler_rcparam_security.rst create mode 100644 doc/users/prev_whats_new/github_stats_3.10.8.rst diff --git a/doc/_static/switcher.json b/doc/_static/switcher.json index 36e743db21b8..cc275467efc4 100644 --- a/doc/_static/switcher.json +++ b/doc/_static/switcher.json @@ -1,7 +1,7 @@ [ { "name": "3.10 (stable)", - "version": "3.10.8", + "version": "3.10.9", "url": "https://matplotlib.org/stable/", "preferred": true }, diff --git a/doc/api/next_api_changes/deprecations/31248-SS.rst b/doc/api/prev_api_changes/api_changes_3.10.9.rst similarity index 71% rename from doc/api/next_api_changes/deprecations/31248-SS.rst rename to doc/api/prev_api_changes/api_changes_3.10.9.rst index 1d7adbdf0fde..592faadc347b 100644 --- a/doc/api/next_api_changes/deprecations/31248-SS.rst +++ b/doc/api/prev_api_changes/api_changes_3.10.9.rst @@ -1,3 +1,11 @@ +API Changes for 3.10.9 +====================== + + +Deprecations +------------ + + Arbitrary code in ``axes.prop_cycle`` rcParam strings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -7,3 +15,6 @@ expressions that previously worked (list comprehensions, for example) no longer will. This change is made without a deprecation period to improve security. The previously documented cycler operations at https://matplotlib.org/cycler/ are still supported. + +This change was originally slated for v3.11.0 of Matplotlib, but was additionally +backported due to the security implications. diff --git a/doc/users/github_stats.rst b/doc/users/github_stats.rst index 8d93759ac756..182f17b5e73c 100644 --- a/doc/users/github_stats.rst +++ b/doc/users/github_stats.rst @@ -1,19 +1,22 @@ .. _github-stats: -GitHub statistics for 3.10.8 (Nov 12, 2025) +GitHub statistics for 3.10.9 (Apr 23, 2026) =========================================== -GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2025/11/12 +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/04/23 These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 4 issues and merged 16 pull requests. -The full list can be seen `on GitHub `__ +We closed 10 issues and merged 34 pull requests. +The full list can be seen `on GitHub `__ -The following 35 authors contributed 445 commits. +The following 37 authors contributed 519 commits. * Aasma Gupta +* Aman Srivastava * Antony Lee +* beelauuu +* Ben Root * Christine P. Chai * David Stansby * dependabot[bot] @@ -21,7 +24,6 @@ The following 35 authors contributed 445 commits. * G.D. McBain * Greg Lucas * hannah -* heinrich5991 * hu-xiaonan * Ian Thomas * Inês Cachola @@ -29,52 +31,76 @@ The following 35 authors contributed 445 commits. * Jouni K. Seppänen * Khushi_29 * Kyle Sunden -* Lucas Gruwez * Lumberbot (aka Jack) +* m-sahare * N R Navaneet * Nathan G. Wiseman -* Nathan Goldbaum -* Nick Coish * Oscar Gustafsson * Praful Gulani * Qian Zhang -* Rafael Katri * Raphael Erik Hviding +* Raphael Quast * Roman * Ruth Comer * saikarna913 * Scott Shambaugh +* Steve Berardi * Thomas A Caswell * Tim Hoffmann * Trygve Magnus Ræder +* Vikash Kumar GitHub issues and pull requests: -Pull Requests (16): +Pull Requests (34): -* :ghpull:`30717`: Backport PR #30714 on branch v3.10.x (FIX: Gracefully handle numpy arrays as input to check_in_list()) -* :ghpull:`30714`: FIX: Gracefully handle numpy arrays as input to check_in_list() -* :ghpull:`30560`: Consistent zoom boxes -* :ghpull:`30711`: Backport PR #30697 on branch v3.10.x (BUG: raise when creating a MacOS FigureManager outside the main thread) -* :ghpull:`30697`: BUG: raise when creating a MacOS FigureManager outside the main thread -* :ghpull:`30656`: Backport PR #29810 on branch v3.10.x (Declare free-threaded support in MacOS backend extension) -* :ghpull:`30702`: Backport PR #30624 on branch v3.10.x (TST: Increase tolerances for Ghostscript 10.06) -* :ghpull:`30700`: Backport PR #30698 on branch v3.10.x (BLD: update trove metadata to support py3.14) -* :ghpull:`30624`: TST: Increase tolerances for Ghostscript 10.06 -* :ghpull:`30698`: BLD: update trove metadata to support py3.14 -* :ghpull:`30688`: Backport PR #30687 on branch v3.10.x (DOC: Fix pip link) -* :ghpull:`30675`: Backport PR #30657 on branch v3.10.x (Fix AttributeError: module 'gi' has no attribute 'require_version') -* :ghpull:`30674`: Backport PR #30672 on branch v3.10.x (Use pathlib.Path instead of matplotlib.path.Path in text.pyi) -* :ghpull:`30672`: Use pathlib.Path instead of matplotlib.path.Path in text.pyi -* :ghpull:`30657`: Fix ``AttributeError: module 'gi' has no attribute 'require_version'`` -* :ghpull:`29810`: Declare free-threaded support in MacOS backend extension +* :ghpull:`31556`: FIX: Inverted PyErr_Occurred check in enum type caster (_enums.h) +* :ghpull:`31078`: Backport PR #31075 on branch v3.10.x (Fix remove method for figure title and xy-labels) +* :ghpull:`31280`: Backport PR #31278 on branch v3.10.x (Fix ``clabel`` manual argument not accepting unit-typed coordinates) +* :ghpull:`31520`: Backport PR #31020 on branch v3.10.x (DOC: Fix doc builds with Sphinx 9) +* :ghpull:`31511`: Backport PR #31504 on branch v3.10.x (Re-order variants to prioritize narrower types) +* :ghpull:`31504`: Re-order variants to prioritize narrower types +* :ghpull:`31445`: Backport PR #31437: mathtext: Fix type inconsistency with fontmaps +* :ghpull:`31437`: mathtext: Fix type inconsistency with fontmaps +* :ghpull:`31411`: Backport PR #31323 on branch v3.10.x (FIX: Prevent crash when removing a subfigure containing subplots) +* :ghpull:`31421`: Backport PR #31420 on branch v3.10.x (Fix outdated Savannah URL for freetype download) +* :ghpull:`31420`: Fix outdated Savannah URL for freetype download +* :ghpull:`31418`: Backport PR #31401: BLD: Temporarily pin setuptools-scm<10 +* :ghpull:`31323`: FIX: Prevent crash when removing a subfigure containing subplots +* :ghpull:`31401`: BLD: Temporarily pin setuptools-scm<10 +* :ghpull:`31278`: Fix ``clabel`` manual argument not accepting unit-typed coordinates +* :ghpull:`31154`: Backport PR #31153 on branch v3.10.x (TST: Use correct method of clearing mock objects) +* :ghpull:`31153`: TST: Use correct method of clearing mock objects +* :ghpull:`31075`: Fix remove method for figure title and xy-labels +* :ghpull:`31036`: Backport PR #31035 on branch v3.10.x (DOCS: Fix typo in time array step size comment) +* :ghpull:`30986`: Backport PR #30985 on branch v3.10.x (MNT: do not assign a numpy array shape) +* :ghpull:`30985`: MNT: do not assign a numpy array shape +* :ghpull:`30971`: Backport PR #30969 on branch v3.10.x (DOC: Simplify barh() example) +* :ghpull:`30965`: Backport PR #30952 on branch v3.10.x (DOC: Tutorial on API shortcuts) +* :ghpull:`30964`: Backport PR #30960 on branch v3.10.x (SVG backend - handle font weight as integer) +* :ghpull:`30960`: SVG backend - handle font weight as integer +* :ghpull:`30924`: Backport PR #30910 on branch v3.10.x (DOC: Improve writer parameter docs of Animation.save()) +* :ghpull:`30870`: Backport PR #30869 on branch v3.10.x (FIX: Accept array for zdir) +* :ghpull:`30869`: FIX: Accept array for zdir +* :ghpull:`30860`: Backport PR #30858 on branch v3.10.x (DOC: reinstate "codex" search term) +* :ghpull:`30818`: Backport PR #30817 on branch v3.10.x (Update sphinx-gallery header patch) +* :ghpull:`30801`: Backport PR #30763 on branch v3.10.x (DOC: Add example how to align tick labels) +* :ghpull:`30791`: Backport PR #30788 on branch v3.10.8-doc (Fix typo in key-mapping for "f11") +* :ghpull:`30790`: Backport PR #30788 on branch v3.10.x (Fix typo in key-mapping for "f11") +* :ghpull:`30788`: Fix typo in key-mapping for "f11" -Issues (4): +Issues (10): -* :ghissue:`30706`: [Bug]: Axes.grouped_bar() with non-string orientation (e.g., NumPy array) raises ambiguous truth-value error instead of clean ValueError -* :ghissue:`30666`: [Bug]: calling pyplot.gca() outside the main thread crashes the interpreter with the MacOS backend -* :ghissue:`30669`: [Bug]: Type hint for fontproperties keyword in text.pyi is wrong -* :ghissue:`30654`: [Bug]: error plotting: AttributeError: module 'gi' has no attribute 'require_version' +* :ghissue:`31495`: Unavoidable warnings with pybind11 main branch +* :ghissue:`31433`: [MNT]: Mypy error +* :ghissue:`31340`: [Bug]: outdated savannah URL in subprojects/freetype-2.6.1.wrap +* :ghissue:`31319`: [Bug]: Crash when removing a subfigure with a subplot in a figure +* :ghissue:`27525`: [Bug]: clabel manual argument does not accept units +* :ghissue:`31112`: [TST] Upcoming dependency test failures +* :ghissue:`31073`: [Bug]: Crash when Removing Suptitle in a Figure with Constrained Layout +* :ghissue:`30981`: [TST] Upcoming dependency test failures +* :ghissue:`30868`: [Bug]: Axe3D text() method does not allow zdir=numpy.array(...) +* :ghissue:`21566`: [ENH]: set_horizontalalignment("right") on Y axis labels when yaxis.ticks_right() is used. Previous GitHub statistics diff --git a/doc/users/next_whats_new/cycler_rcparam_security.rst b/doc/users/next_whats_new/cycler_rcparam_security.rst deleted file mode 100644 index e4e2893aa994..000000000000 --- a/doc/users/next_whats_new/cycler_rcparam_security.rst +++ /dev/null @@ -1,14 +0,0 @@ -``axes.prop_cycle`` rcParam security improvements -------------------------------------------------- - -The ``axes.prop_cycle`` rcParam is now parsed in a safer and more restricted -manner. Only literals, ``cycler()`` and ``concat()`` calls, the operators -``+`` and ``*``, and slicing are allowed. All previously valid cycler strings -documented at https://matplotlib.org/cycler/ are still supported, for example: - -.. code-block:: none - - axes.prop_cycle : cycler('color', ['r', 'g', 'b']) + cycler('linewidth', [1, 2, 3]) - axes.prop_cycle : 2 * cycler('color', 'rgb') - axes.prop_cycle : concat(cycler('color', 'rgb'), cycler('color', 'cmk')) - axes.prop_cycle : cycler('color', 'rgbcmk')[:3] diff --git a/doc/users/prev_whats_new/github_stats_3.10.8.rst b/doc/users/prev_whats_new/github_stats_3.10.8.rst new file mode 100644 index 000000000000..dd3d19e036eb --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.10.8.rst @@ -0,0 +1,77 @@ +.. _github-stats_3-10-8: + +GitHub statistics for 3.10.8 (Nov 12, 2025) +=========================================== + +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2025/11/12 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 4 issues and merged 16 pull requests. +The full list can be seen `on GitHub `__ + +The following 35 authors contributed 445 commits. + +* Aasma Gupta +* Antony Lee +* Christine P. Chai +* David Stansby +* dependabot[bot] +* Elliott Sales de Andrade +* G.D. McBain +* Greg Lucas +* hannah +* heinrich5991 +* hu-xiaonan +* Ian Thomas +* Inês Cachola +* Jody Klymak +* Jouni K. Seppänen +* Khushi_29 +* Kyle Sunden +* Lucas Gruwez +* Lumberbot (aka Jack) +* N R Navaneet +* Nathan G. Wiseman +* Nathan Goldbaum +* Nick Coish +* Oscar Gustafsson +* Praful Gulani +* Qian Zhang +* Rafael Katri +* Raphael Erik Hviding +* Roman +* Ruth Comer +* saikarna913 +* Scott Shambaugh +* Thomas A Caswell +* Tim Hoffmann +* Trygve Magnus Ræder + +GitHub issues and pull requests: + +Pull Requests (16): + +* :ghpull:`30717`: Backport PR #30714 on branch v3.10.x (FIX: Gracefully handle numpy arrays as input to check_in_list()) +* :ghpull:`30714`: FIX: Gracefully handle numpy arrays as input to check_in_list() +* :ghpull:`30560`: Consistent zoom boxes +* :ghpull:`30711`: Backport PR #30697 on branch v3.10.x (BUG: raise when creating a MacOS FigureManager outside the main thread) +* :ghpull:`30697`: BUG: raise when creating a MacOS FigureManager outside the main thread +* :ghpull:`30656`: Backport PR #29810 on branch v3.10.x (Declare free-threaded support in MacOS backend extension) +* :ghpull:`30702`: Backport PR #30624 on branch v3.10.x (TST: Increase tolerances for Ghostscript 10.06) +* :ghpull:`30700`: Backport PR #30698 on branch v3.10.x (BLD: update trove metadata to support py3.14) +* :ghpull:`30624`: TST: Increase tolerances for Ghostscript 10.06 +* :ghpull:`30698`: BLD: update trove metadata to support py3.14 +* :ghpull:`30688`: Backport PR #30687 on branch v3.10.x (DOC: Fix pip link) +* :ghpull:`30675`: Backport PR #30657 on branch v3.10.x (Fix AttributeError: module 'gi' has no attribute 'require_version') +* :ghpull:`30674`: Backport PR #30672 on branch v3.10.x (Use pathlib.Path instead of matplotlib.path.Path in text.pyi) +* :ghpull:`30672`: Use pathlib.Path instead of matplotlib.path.Path in text.pyi +* :ghpull:`30657`: Fix ``AttributeError: module 'gi' has no attribute 'require_version'`` +* :ghpull:`29810`: Declare free-threaded support in MacOS backend extension + +Issues (4): + +* :ghissue:`30706`: [Bug]: Axes.grouped_bar() with non-string orientation (e.g., NumPy array) raises ambiguous truth-value error instead of clean ValueError +* :ghissue:`30666`: [Bug]: calling pyplot.gca() outside the main thread crashes the interpreter with the MacOS backend +* :ghissue:`30669`: [Bug]: Type hint for fontproperties keyword in text.pyi is wrong +* :ghissue:`30654`: [Bug]: error plotting: AttributeError: module 'gi' has no attribute 'require_version' diff --git a/doc/users/release_notes.rst b/doc/users/release_notes.rst index dbdcd61437ac..d952093ca254 100644 --- a/doc/users/release_notes.rst +++ b/doc/users/release_notes.rst @@ -18,10 +18,12 @@ Version 3.10 :maxdepth: 1 prev_whats_new/whats_new_3.10.0.rst + ../api/prev_api_changes/api_changes_3.10.9.rst ../api/prev_api_changes/api_changes_3.10.7.rst ../api/prev_api_changes/api_changes_3.10.1.rst ../api/prev_api_changes/api_changes_3.10.0.rst github_stats.rst + prev_whats_new/github_stats_3.10.8.rst prev_whats_new/github_stats_3.10.7.rst prev_whats_new/github_stats_3.10.6.rst prev_whats_new/github_stats_3.10.5.rst From dd8d78b8dce60b6c8db86132892577a0b9dbe469 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 23 Apr 2026 17:48:02 -0500 Subject: [PATCH 29/30] REL: v3.10.9 This is a patch release of the v3.10.x series. Highlights of this release include: - Various minor bug and doc fixes - Security hardening validation of cyclers - Removing eval usage - Security hardening in Latex and PS calls - Removing shell escapes From 58b6023db902bf5683c08359931a7799c5398450 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 23 Apr 2026 17:59:12 -0500 Subject: [PATCH 30/30] REL: Bump from v3.10.9