Azure pipeline is failing randomly when using `plt.subplots` because of `Tcl`

Hello,

After a lot of test… I still can not figure out why the Azure pipeline for a repository I maintain is randomly, and I insist on the randomly, failing. I would appreciate any help from experienced dev!

I’m running a pipeline on the windows-latest image, with python versions ranging from 3.7 to 3.10. Here is the pipeline: pycrostates/azure-pipelines.yml at 555ef8db65755dd415ae075e53b7cd4b03f69edf · vferat/pycrostates · GitHub
It’s nothing fancy, it retrieves from cache a testing dataset or downloads it and then runs the unit tests with pytest. But, the windows runner will randomly fail during the tests (usually 1 or 2 out of the 4 python versions) with 2 errors related to matplotlib and Tcl.

The first one: _tkinter.TclError: invalid command name "tcl_findLibrary"

    def test_plot_segmentation(ModK, inst):
        """Test the plot of a segmentation."""
        segmentation = ModK.predict(inst)
    
        segmentation.plot()
        plt.close("all")
>       segmentation.plot(cmap="plasma")

pycrostates\segmentation\tests\test_segmentation.py:194: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
pycrostates\segmentation\segmentation.py:395: in plot
    verbose=verbose,
pycrostates\viz\segmentation.py:77: in plot_raw_segmentation
    **kwargs,
pycrostates\viz\segmentation.py:205: in _plot_segmentation
    fig, axes = plt.subplots(1, 1)
C:\hostedtoolcache\windows\Python\3.7.9\x64\lib\site-packages\matplotlib\pyplot.py:1455: in subplots
    fig = figure(**fig_kw)
C:\hostedtoolcache\windows\Python\3.7.9\x64\lib\site-packages\matplotlib\pyplot.py:811: in figure
    FigureClass=FigureClass, **kwargs)
C:\hostedtoolcache\windows\Python\3.7.9\x64\lib\site-packages\matplotlib\pyplot.py:327: in new_figure_manager
    return _get_backend_mod().new_figure_manager(*args, **kwargs)
C:\hostedtoolcache\windows\Python\3.7.9\x64\lib\site-packages\matplotlib\backend_bases.py:3494: in new_figure_manager
    return cls.new_figure_manager_given_figure(num, fig)
C:\hostedtoolcache\windows\Python\3.7.9\x64\lib\site-packages\matplotlib\backends\_backend_tk.py:949: in new_figure_manager_given_figure
    window = tk.Tk(className="matplotlib")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <tkinter.Tk object .>, screenName = None, baseName = 'pytest'
className = 'matplotlib', useTk = 1, sync = 0, use = None

    def __init__(self, screenName=None, baseName=None, className='Tk',
                 useTk=1, sync=0, use=None):
        """Return a new Toplevel widget on screen SCREENNAME. A new Tcl interpreter will
        be created. BASENAME will be used for the identification of the profile file (see
        readprofile).
        It is constructed from sys.argv[0] without extensions if None is given. CLASSNAME
        is the name of the widget class."""
        self.master = None
        self.children = {}
        self._tkloaded = 0
        # to avoid recursions in the getattr code in case of failure, we
        # ensure that self.tk is always _something_.
        self.tk = None
        if baseName is None:
            import os
            baseName = os.path.basename(sys.argv[0])
            baseName, ext = os.path.splitext(baseName)
            if ext not in ('.py', '.pyc'):
                baseName = baseName + ext
        interactive = 0
>       self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
E       _tkinter.TclError: invalid command name "tcl_findLibrary"

Note that it does not always occur in the same test, e.g. same error in a different run:

    def test_check_axes():
        """Test _check_axes checker."""
        # test valid inputs
>       _, ax = plt.subplots(1, 1)

pycrostates\utils\tests\test_checks.py:114: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\hostedtoolcache\windows\Python\3.8.10\x64\lib\site-packages\matplotlib\pyplot.py:1455: in subplots
    fig = figure(**fig_kw)
C:\hostedtoolcache\windows\Python\3.8.10\x64\lib\site-packages\matplotlib\pyplot.py:808: in figure
    manager = new_figure_manager(
C:\hostedtoolcache\windows\Python\3.8.10\x64\lib\site-packages\matplotlib\pyplot.py:327: in new_figure_manager
    return _get_backend_mod().new_figure_manager(*args, **kwargs)
C:\hostedtoolcache\windows\Python\3.8.10\x64\lib\site-packages\matplotlib\backend_bases.py:3494: in new_figure_manager
    return cls.new_figure_manager_given_figure(num, fig)
C:\hostedtoolcache\windows\Python\3.8.10\x64\lib\site-packages\matplotlib\backends\_backend_tk.py:949: in new_figure_manager_given_figure
    window = tk.Tk(className="matplotlib")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <tkinter.Tk object .>, screenName = None, baseName = 'pytest'
className = 'matplotlib', useTk = 1, sync = 0, use = None

    def __init__(self, screenName=None, baseName=None, className='Tk',
                 useTk=1, sync=0, use=None):
        """Return a new Toplevel widget on screen SCREENNAME. A new Tcl interpreter will
        be created. BASENAME will be used for the identification of the profile file (see
        readprofile).
        It is constructed from sys.argv[0] without extensions if None is given. CLASSNAME
        is the name of the widget class."""
        self.master = None
        self.children = {}
        self._tkloaded = False
        # to avoid recursions in the getattr code in case of failure, we
        # ensure that self.tk is always _something_.
        self.tk = None
        if baseName is None:
            import os
            baseName = os.path.basename(sys.argv[0])
            baseName, ext = os.path.splitext(baseName)
            if ext not in ('.py', '.pyc'):
                baseName = baseName + ext
        interactive = 0
>       self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
E       _tkinter.TclError: invalid command name "tcl_findLibrary"

And the second error: _tkinter.TclError: Can't find a usable init.tcl

    def test_check_axes():
        """Test _check_axes checker."""
        # test valid inputs
>       _, ax = plt.subplots(1, 1)

pycrostates\utils\tests\test_checks.py:114: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\hostedtoolcache\windows\Python\3.9.13\x64\lib\site-packages\matplotlib\pyplot.py:1455: in subplots
    fig = figure(**fig_kw)
C:\hostedtoolcache\windows\Python\3.9.13\x64\lib\site-packages\matplotlib\pyplot.py:808: in figure
    manager = new_figure_manager(
C:\hostedtoolcache\windows\Python\3.9.13\x64\lib\site-packages\matplotlib\pyplot.py:327: in new_figure_manager
    return _get_backend_mod().new_figure_manager(*args, **kwargs)
C:\hostedtoolcache\windows\Python\3.9.13\x64\lib\site-packages\matplotlib\backend_bases.py:3494: in new_figure_manager
    return cls.new_figure_manager_given_figure(num, fig)
C:\hostedtoolcache\windows\Python\3.9.13\x64\lib\site-packages\matplotlib\backends\_backend_tk.py:949: in new_figure_manager_given_figure
    window = tk.Tk(className="matplotlib")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <tkinter.Tk object .>, screenName = None, baseName = 'pytest'
className = 'matplotlib', useTk = True, sync = False, use = None

    def __init__(self, screenName=None, baseName=None, className='Tk',
                 useTk=True, sync=False, use=None):
        """Return a new Toplevel widget on screen SCREENNAME. A new Tcl interpreter will
        be created. BASENAME will be used for the identification of the profile file (see
        readprofile).
        It is constructed from sys.argv[0] without extensions if None is given. CLASSNAME
        is the name of the widget class."""
        self.master = None
        self.children = {}
        self._tkloaded = False
        # to avoid recursions in the getattr code in case of failure, we
        # ensure that self.tk is always _something_.
        self.tk = None
        if baseName is None:
            import os
            baseName = os.path.basename(sys.argv[0])
            baseName, ext = os.path.splitext(baseName)
            if ext not in ('.py', '.pyc'):
                baseName = baseName + ext
        interactive = False
>       self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
E       _tkinter.TclError: Can't find a usable init.tcl in the following directories: 
E           {C:\hostedtoolcache\windows\Python\3.9.13\x64\tcl\tcl8.6}
E       
E       C:/hostedtoolcache/windows/Python/3.9.13/x64/tcl/tcl8.6/init.tcl: couldn't read file "C:/hostedtoolcache/windows/Python/3.9.13/x64/tcl/tcl8.6/init.tcl": No error
E       couldn't read file "C:/hostedtoolcache/windows/Python/3.9.13/x64/tcl/tcl8.6/init.tcl": No error
E           while executing
E       "uplevel #0 [list source $tclfile]"
E       
E       
E       This probably means that Tcl wasn't installed properly.

I am completely out of idea, does anyone has a good suggestion on how to fix it and make this CI workflow stable and reliable?

I will add one more piece of information I forgot… I did see it fail on the second call to a plt.subplots() in a test, meaning the first one actually worked! e.g.

    def test_plot_epoch_segmentation():
        """Test segmentation plots for epochs."""
        n_clusters = 4
        labels = np.random.choice(
            [-1, 0, 1, 2, 3], (len(epochs), epochs.times.size)
        )
    
        plot_epoch_segmentation(labels, epochs, n_clusters)
        plt.close("all")
    
        # provide ax
        f, ax = plt.subplots(1, 1)
        plot_epoch_segmentation(labels, epochs, n_clusters, axes=ax)
        plt.close("all")
    
        # provide cbar_ax
>>      f, cbar_ax = plt.subplots(1, 1)            >> Failed here.

Do you need an interactive backend for your tests? If not, would it not be better to use agg?

No, there is probably no need for the interactive aspect. So you are suggesting setting the env variable MPLBACKEND to agg? Does this looks correct?

job: pytest
    variables:
      MPLBACKEND: agg
   ...

Actually no, I’m wrong, I do need interactiveness because I’m using plt.show() often to show a figure for the user.

Using Agg and disabling the UserWarning for matplotlib in pytest is a good enough fix for me at the moment. Thanks for the suggestion!