12 September 2026 · Syamjith NK
On 8 September I filed four issues about the same defect: Python code that pre-shapes Arabic
with arabic_reshaper.reshape() and bidi.get_display() before handing it
to matplotlib, which has shaped text itself since 3.11. The workaround that used to be necessary
now reverses the string.
This morning two of them were closed as completed by the same maintainer, justncodes: whiteout-project/bot#110 and kingshot-project/Kingshot-Discord-Bot#28. Those are two of his own projects, sibling codebases carrying the same helper on the same line, and he closed them three minutes apart with word-for-word the same comment. So this is one maintainer acting twice, not two independent confirmations, and I am not going to count it as two. The interesting part is not that they were fixed. It is that he did not use the fix my linter performs, and reading his reason showed me the tool is overconfident.
Both bots draw charts of player activity, and player names are often Arabic. Both route those
names through a helper carrying the docstring “Shape Arabic for matplotlib (which
doesn’t run bidi itself)”, which was true for about a decade and stopped being
true in matplotlib 3.11. Their pin was matplotlib>=3.10.5 with no upper bound, so
a fresh install resolves to 3.11 and the shaping happens twice.
I did not try to prove that with a number. An earlier attempt at a cross-renderer pixel metric scored the correct render worse than the broken one, because normalising two different ink boxes to a fixed size warps them differently. What settles it is rendering both and looking: محمد الفارس comes out as سرافلا دمحم. Letters and word order both reversed. Any Arabic reader confirms that in a second, and the issue quotes those two strings rather than a statistic.
arabic-lint --fix deletes the pre-shaping. Where the whole recipe sits in one
expression, the rewrite is provably correct and the tool applies it:
- return _bidi_get_display(_arabic_reshaper.reshape(text))
+ return text
It refuses the split form, where reshape() and get_display() are on
separate lines, because removing only the second call strips the reordering and leaves the shaping
applied: still wrong, but now looking fixed. I was fairly pleased with that piece of restraint.
It turns out to be restraint about the wrong thing.
His reply, in full, on both issues:
Confirmed, and thanks for the thorough report. Our updater only installs missing packages rather than enforcing version bumps, so requiring 3.11 would have left existing installs on 3.10 with no shaping at all; we’ve gone with a version gate instead (pass through on 3.11+, pre-shape below). The same pattern also existed in our attendance table reports and is fixed there too. Will ship in the next release.
The shape of it, in my words rather than his code:
if matplotlib_version >= (3, 11):
return text
return get_display(reshape(text))
My deletion assumes the installed matplotlib is 3.11 or newer. For these projects that assumption does not hold, and the reason is nowhere in the source: their updater installs missing packages and never bumps an existing one. Thousands of running installs sit on 3.10 and will stay there. Delete the call on those and Arabic names do not come out reversed any more, they come out unshaped, as disconnected letters in the wrong order. I would have traded one rendering bug for another and told them it was a fix.
The correct fix depends on whether the project controls its own dependency floor.
| project | can it force matplotlib 3.11? | correct fix |
|---|---|---|
| Application with a lockfile or pinned requirements | yes | delete the pre-shaping |
| Library on PyPI | yes, by raising its floor in metadata | delete it and raise the floor |
| Self-hosted app that updates itself without bumping versions | no | version gate |
| Anything a user installs by hand from a README | no | version gate |
A static analyser reads the file. It cannot read the distribution model, and the distribution
model is what decides this. So the honest thing for the tool to report is not remove this
line, it is: this pre-shaping is wrong on matplotlib 3.11 and newer, and here are two ways to
resolve it depending on whether you can guarantee the version your users have. That is a
correction to --fix, not to the detection, and it is the next thing I will change.
The check found a real bug in both repos. The remedy it attached to that bug was too confident
about somebody else’s deployment.
One line of his reply is easy to skip: “The same pattern also existed in our attendance table reports and is fixed there too.”
I never mentioned that code. I reported one helper in one file, because that is the one I could verify by reading their source from outside. He knew where else the pattern lived in his own project, and fixed both in the same pass.
That is worth remembering before spending an afternoon enumerating every call site in a stranger’s repository. What a bug report has to deliver is the diagnosis: what is wrong, why it is wrong now when it was right before, and how to tell. The maintainer supplies the map. Mine was one helper and a version boundary, and it reached code I never saw.
Four were filed. Two are fixed, and two have had no reply at all: ComfyUI-PersianText#3 (the Pillow-with-Raqm version of the same mistake) and NoorBayan/Diwan#3, an Arabic poetry dataset whose chart labels and titles are reversed on any current install.
They may get fixed, or they may sit open for a year. Filing an issue is not an outcome, it is a message to a volunteer with their own backlog, and half of mine have not been read. The two that were read got answered in four days by someone who understood the report faster than I wrote it, then improved on the fix I suggested. That is a better result than the one I was expecting, and the useful part of it is the correction.