Repository: matplotlib/matplotlib

Issue:
[Bug]: figure with subfigures cannot be pickled
### Bug summary

I tried to pickle figures with subfigures nested.
Returns error with the following code.

### Code for reproduction

```Python
#!/usr/bin/env python3
import pickle
import matplotlib.pyplot as plt

fig = plt.figure()
subfigs = fig.subfigures(2,2)
with open('fig.p', 'wb') as file:
    pickle.dump(fig, file)
```


### Actual outcome
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[1], line 8
      6 subfigs = fig.subfigures(2,2)
      7 with open('fig.p', 'wb') as file:
----> 8     pickle.dump(fig, file)

TypeError: cannot pickle 'FigureCanvasQTAgg' object
```

### Expected outcome

A pickle file should be saved to the working directory.

### Additional information

I know the API is still provisional but I couldn't find other reports so I decided to post.
I believe this is caused by the "subfigures" API, as skipping that line makes the script work.
It's my first time to report a issue, so I'm a bit worried if I'm doing things right.

### Operating system

Windows 10

### Matplotlib Version

3.9.0

### Matplotlib Backend

qtagg

### Python version

3.12.1

### Jupyter version

_No response_

### Installation

pip


Discussion:

Patch:
diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py
index 9139b2ed262f..7bb06814f389 100644
--- a/lib/matplotlib/figure.py
+++ b/lib/matplotlib/figure.py
@@ -2227,7 +2227,6 @@ def __init__(self, parent, subplotspec, *,
         self.subplotpars = parent.subplotpars
         self.dpi_scale_trans = parent.dpi_scale_trans
         self._axobservers = parent._axobservers
-        self.canvas = parent.canvas
         self.transFigure = parent.transFigure
         self.bbox_relative = Bbox.null()
         self._redo_transform_rel_fig()
@@ -2244,6 +2243,10 @@ def __init__(self, parent, subplotspec, *,
         self._set_artist_props(self.patch)
         self.patch.set_antialiased(False)
 
+    @property
+    def canvas(self):
+        return self._parent.canvas
+
     @property
     def dpi(self):
         return self._parent.dpi
diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi
index 21de9159d56c..b079312695c1 100644
--- a/lib/matplotlib/figure.pyi
+++ b/lib/matplotlib/figure.pyi
@@ -263,7 +263,6 @@ class SubFigure(FigureBase):
     figure: Figure
     subplotpars: SubplotParams
     dpi_scale_trans: Affine2D
-    canvas: FigureCanvasBase
     transFigure: Transform
     bbox_relative: Bbox
     figbbox: BboxBase
@@ -282,6 +281,8 @@ class SubFigure(FigureBase):
         **kwargs
     ) -> None: ...
     @property
+    def canvas(self) -> FigureCanvasBase: ...
+    @property
     def dpi(self) -> float: ...
     @dpi.setter
     def dpi(self, value: float) -> None: ...


Test patch:
diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py
index 7e7ccc14bf8f..0cba4f392035 100644
--- a/lib/matplotlib/tests/test_pickle.py
+++ b/lib/matplotlib/tests/test_pickle.py
@@ -93,6 +93,11 @@ def _generate_complete_test_figure(fig_ref):
     plt.errorbar(x, x * -0.5, xerr=0.2, yerr=0.4, label='$-.5 x$')
     plt.legend(draggable=True)
 
+    # Ensure subfigure parenting works.
+    subfigs = fig_ref.subfigures(2)
+    subfigs[0].subplots(1, 2)
+    subfigs[1].subplots(1, 2)
+
     fig_ref.align_ylabels()  # Test handling of _align_label_groups Groupers.