Skip to content

Error with ipykernel>=7 in combination with plt.tight_layout #610

Description

@jokasimr

Describe the issue

Running the following in a Jupyter lab cell

%matplotlib ipympl
import numpy as np
import matplotlib.pyplot as plt

da = np.random.random((100, 100))
da[0, 0] = 1e-23

plt.figure()
ax = plt.pcolormesh(da, norm='log')
plt.colorbar()

plt.figure()
ax = plt.pcolormesh(da, norm='log')
plt.colorbar()

plt.tight_layout()

raises

File ~/.local/share/mamba/envs/testing3/lib/python3.11/site-packages/matplotlib/_mathtext.py:2167, in Parser.parse(self, s, fonts_object, fontsize, dpi)
   2164     result = self._expression.parse_string(s)
   2165 except ParseBaseException as err:
   2166     # explain becomes a plain method on pyparsing 3 (err.explain(0)).
-> 2167     raise ValueError("\n" + ParseException.explain(err, 0)) from None
   2168 self._state_stack = []
   2169 self._in_subscript_or_superscript = False

ValueError: 
$\mathdefault{10^{-2}}$
^
ParseException: Expected end of text, found '$'  (at char 0), (line:1, col:1)

To reproduce
Create an environment using the follwing command:

mamba create -n testing3 python=3.11 matplotlib ipympl numpy jupyter

activate the environment, start Jupyter Lab, and run the cell above.

The error does not happen without %matplotlib ipympl and it does not happen if you restrict the environment to use ipykernel<7.

Versions

(testing3) ~ $ python -c "import sys; print('\n',sys.version); import ipympl; print('ipympl version:', ipympl.__version__)" && jupyter --version && jupyter nbextension list && jupyter labextension list

 3.11.14 | packaged by conda-forge | (main, Oct 22 2025, 22:46:25) [GCC 14.3.0]
ipympl version: 0.9.8
Selected Jupyter core packages...
IPython          : 9.7.0
ipykernel        : 7.1.0
ipywidgets       : 8.1.8
jupyter_client   : 8.6.3
jupyter_core     : 5.9.1
jupyter_server   : 2.17.0
jupyterlab       : 4.4.10
nbclient         : 0.10.2
nbconvert        : 7.16.6
nbformat         : 5.10.4
notebook         : 7.4.7
qtconsole        : not installed
traitlets        : 5.14.3
usage: jupyter [-h] [--version] [--config-dir] [--data-dir] [--runtime-dir]
               [--paths] [--json] [--debug]
               [subcommand]

Jupyter: Interactive Computing

positional arguments:
  subcommand     the subcommand to launch

options:
  -h, --help     show this help message and exit
  --version      show the versions of core jupyter packages and exit
  --config-dir   show Jupyter config dir
  --data-dir     show Jupyter data dir
  --runtime-dir  show Jupyter runtime dir
  --paths        show all Jupyter paths. Add --json for machine-readable
                 format.
  --json         output paths as machine-readable json
  --debug        output debug information about paths

Available subcommands: console dejavu events execute kernel kernelspec lab
labextension labhub migrate nbconvert notebook run server troubleshoot trust

Jupyter command `jupyter-nbextension` not found.

Activity

  1. MartinKorz commented on Jul 17, 2026

    @MartinKorz

    We hit this bug and debugged it beyond the MathTextParser lock, which turned out to be insufficient

    Root cause: JupyterLab ≥ 4.4 defaults commsOverSubshells to perCommTarget, and ipykernel ≥ 7 runs each kernel subshell on its own thread. Widget comm messages — including ipympl's canvas draw/resize/frame requests — are then serviced on a subshell thread concurrently with cell execution on the main thread. matplotlib is not thread-safe, so any live canvas racing a main-thread figure build corrupts shared state. This matches the ipykernel≥7 correlation reported here: with ipykernel < 7 (or commsOverSubshells: "disabled") the concurrency doesn't exist.

    The mathtext ValueError is only the first symptom. We serialized successive chokepoints with a shared process-global RLock and watched the failure move each time:

    1. Locking MathTextParser.parse (equivalent to PR monkeypatch _mathtext.Parser.parse() with a threading.Lock() #621's lock) removes the ValueError — and the race resurfaces as blank canvases and hard kernel crashes.
    2. Additionally locking FigureCanvasAgg.draw helps — and the race resurfaces as RuntimeError: ... did not call Figure.draw, because backend_bases._get_renderer temporarily monkeypatches figure.draw during print_figure(bbox_inches='tight'); a concurrent draw hits the raise-to-capture hook (silently blank frame), and two overlapping windows restore the real draw mid-print.
    3. Additionally locking FigureCanvasBase.print_figure helps again — but the frame path still can't be covered from outside: get_diff_image() reads renderer.buffer_rgba() and handle_resize mutates the figure on the comm thread, with no stable lockable API. On top of that, lock contention itself can starve the comm thread during long runs, so every canvas stays blank until execution finishes anyway.

    So a parse-only lock will reduce but not eliminate the breakage; a complete kernel-side fix would need ipympl's own message handlers (including frame production and resize) serialized against draws — or better, a way for ipympl's comms to opt out of subshells per comm target so they stay on the main shell.

    Verified workarounds: JupyterLab Settings Editor → commsOverSubshells: "disabled", then restart JupyterLab (it only applies to newly connected kernels) — fully stable afterwards, canvases fill in when the cell finishes (pre-4.4 semantics). Pinning ipykernel<7 works too.

    Environment: ipympl 0.10.0, matplotlib 3.11.0, ipykernel 7.3.0, JupyterLab 4.6.1, Python 3.14.

    (The above text was formulated by LLM. I read it multiple times and can confirm all parts that I understand, but I do not have a strong understanding of the code involved and cannot personally verify the suggested ipympl-side solution)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions