3 September 2026
If you have ever drawn Arabic text in Python, you have met this line:
from arabic_reshaper import reshape
from bidi.algorithm import get_display
label = get_display(reshape(arabic_text))
It is in a thousand StackOverflow answers. GitHub's public code index has 1,180 files containing get_display(arabic_reshaper.reshape, and 4,024 that call arabic_reshaper.reshape at all. The package is downloaded millions of times a month.
In matplotlib 3.11 it renders the text backwards.
Not with an error. Not with a warning. The label appears, the glyphs are Arabic, the characters connect. If you do not read Arabic it looks completely fine — and that is the entire problem. Every other kind of text bug announces itself with tofu boxes or a traceback. This one produces something that looks like a language.
matplotlib merged #30000, "Implement text shaping with libraqm", in September 2025. From 3.11, matplotlib shapes and reorders Arabic itself.
So the old recipe now runs twice. reshape() converts the string to presentation forms and get_display() puts it into visual order — then matplotlib applies the bidi algorithm to a string that is already visual, and reverses it.
The workaround did not become unnecessary. It became harmful.
Measured as mean absolute pixel difference against a Pillow/Raqm render of the same string in the same font, crops normalised to 64px:
| input | mean abs diff |
|---|---|
| raw logical string | 7.75 (antialiasing only — correct) |
| pre-shaped with the recipe | 76.28 (reversed) |
And arabic_reshaper appears nowhere in the matplotlib repository, so nothing tells someone upgrading that the line they copied in 2019 is now doing damage.
Worth separating, because it decides whether tooling can save you:
Shaping is detectable. reshape() emits characters from Unicode's Arabic Presentation Forms blocks — legacy compatibility ranges that typed Arabic never uses. Over 1,856 sampled strings it produced 122 distinct Forms-B codepoints. Forms-A shows up only for specific sequences: "الله" collapses to the single codepoint U+FDF2, and two Uighur alef forms appear. So a linter can spot a pre-shaped string with near-certainty.
Reordering is not. get_display() reverses the order of the same codepoints. A reversed string and a correct one contain exactly the same characters. There is no general detector, because there is nothing to detect — only a human who reads the language can tell you which way round it goes.
That asymmetry is why "just add a check" does not work, and why this survived years of being copied.
I filed it. Two projects, two different outcomes, both better than I expected.
Pillow — the same class of problem, different mechanism: Pillow's behaviour depends on whether it was built with Raqm, so the identical script produces correct text on one machine and broken text on another. On a Raqm-enabled build I measured 15 Arabic strings: 0 of 15 render the same once the recipe is applied. Every one is changed by a line that is supposed to be a no-op at worst. The maintainer pushed back hard on my first draft, correctly, for overclaiming a security angle. I dropped it. The warning that remains is narrower and true.
matplotlib — three maintainers replied within two days, including the project lead. One of them, rcomer, went and edited the StackOverflow answers herself to say the workaround is only for older versions. That is by far the widest-reaching result of any of this, and it cost nothing but filing accurately.
The project lead proposed a fix I had not thought of: wrap pre-processed text in Unicode LRO/PDF (U+202D / U+202C), which renders correctly on every matplotlib version. He said he did not know enough Arabic to be sure it was right. I could check, so I did — across three fonts and nine strings. It reads correctly in all of them. It does not always render identically: the wrapped string draws the font's static presentation forms rather than the font's own shaping, so joins loosen and the line widens where those differ. Arial Unicode was unaffected; Geeza Pro differed on every string tested.
That exchange is the whole argument for filing upstream rather than writing a blog post first. He had the better fix. I had the only way to verify it.
features.check("raqm"). If it is False you have a different problemand the recipe will not fully save you.
write will tell you the text came out backwards.
---