TclError from TkAgg in headless CI: where should matplotlib's Agg backend be pinned?
0 reputation · 06 Jan 2024, 21:35 UTC
0 reputation · 06 Jan 2024, 21:35 UTC
Matplotlib's backend abstraction separates rendering from the output device: the Agg backend rasterizes to files without a display, while interactive backends such as TkAgg require a windowing system. In headless CI or Docker images, an auto-selected interactive backend is the documented trigger for a TclError at figure-render time, so a plotting module that behaves normally on developer desktops can fail as soon as it runs in a container.
The remedy direction is clear — pin the non-interactive Agg backend — but the enforcement point is the open decision. matplotlib.use() must run before pyplot is imported; calling it afterwards typically fails to switch backends and only emits a runtime warning. In a codebase where several modules import pyplot at load time, a single early use() call is fragile, and per-developer matplotlibrc files undermine repeatability.
Assume matplotlib 3.x, a pinned container image, and a requirement that the same code still opens interactive windows on developer machines.
matplotlib.use() call in code?matplotlib.get_backend() serve as a reliable runtime assertion in tests that the effective backend is Agg?32775 reputation · 07 Jan 2024, 01:24 UTC
For matplotlib 3.x with a pinned container image and the requirement to keep interactive windows on developer machines, force Agg only in headless CI via the process environment. Set MPLBACKEND=Agg in the Dockerfile or CI job before Python starts. Do not hardcode matplotlib.use('Agg') in library or application modules.
The container inherits a default or user‑configured interactive backend such as TkAgg. TkAgg requires a windowing system and a functional Tcl/Tk installation. In a headless CI or Docker image without a display, figure rendering triggers a TclError even though the same code works on developer desktops.
matplotlib.use(backend) must run before the first import of matplotlib.pyplot or any module that imports it. After pyplot is imported, a non‑force call warns and does not switch backends.MPLBACKEND is read at process start and selects the backend before any import occurs. An explicit matplotlib.use() call in code overrides MPLBACKEND for that process.matplotlib.get_backend() reports the effective backend name after imports. It can be used as a runtime assertion in tests, but a passing check only confirms the name, not that rendering will succeed.force=True is not a reliable substitute and can leave figure manager state inconsistent.# Dockerfile or CI step
ENV MPLBACKEND=Agg
This makes every module that imports matplotlib see Agg without changing application code.
Do not add a top‑level matplotlib.use('Agg') in shared modules. That would prevent interactive windows on developer machines.
If a test entry point must guarantee headless rendering, import a small pin module before any pyplot import and assert the backend in CI only.
import matplotlib
backend = matplotlib.get_backend().lower()
assert backend == 'agg', f'Expected Agg, got {backend}'
python -c \"import matplotlib; print(matplotlib.get_backend())\"
# expect Agg
python -c \"import matplotlib.pyplot as plt; plt.plot([0,1]); plt.savefig('/tmp/agg_check.png')\"
# expect exit 0 and non‑empty PNG
One diagnostic that changes the recommendation: do any library modules import matplotlib.pyplot at import time rather than lazily inside functions? If yes, import order must be restructured or the environment variable is the only reliable enforcement point.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.