Repository navigation
Error with ipykernel>=7 in combination with plt.tight_layout #610
Description
Activity
We hit this bug and debugged it beyond the
MathTextParserlock, which turned out to be insufficientRoot cause: JupyterLab ≥ 4.4 defaults
commsOverSubshellstoperCommTarget, 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 (orcommsOverSubshells: "disabled") the concurrency doesn't exist.The mathtext
ValueErroris only the first symptom. We serialized successive chokepoints with a shared process-global RLock and watched the failure move each time:- Locking
MathTextParser.parse(equivalent to PR monkeypatch _mathtext.Parser.parse() with a threading.Lock() #621's lock) removes theValueError— and the race resurfaces as blank canvases and hard kernel crashes. - Additionally locking
FigureCanvasAgg.drawhelps — and the race resurfaces asRuntimeError: ... did not call Figure.draw, becausebackend_bases._get_renderertemporarily monkeypatchesfigure.drawduringprint_figure(bbox_inches='tight'); a concurrent draw hits the raise-to-capture hook (silently blank frame), and two overlapping windows restore the realdrawmid-print. - Additionally locking
FigureCanvasBase.print_figurehelps again — but the frame path still can't be covered from outside:get_diff_image()readsrenderer.buffer_rgba()andhandle_resizemutates 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). Pinningipykernel<7works 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)- Locking
Describe the issue
Running the following in a Jupyter lab cell
raises
To reproduce
Create an environment using the follwing command:
activate the environment, start Jupyter Lab, and run the cell above.
The error does not happen without
%matplotlib ipympland it does not happen if you restrict the environment to useipykernel<7.Versions