← Syamjith NK

10 September 2026 · Syamjith NK

I scanned 341 Arabic datasets to see if the corruption reaches training data

Three posts on this blog argue that the standard way to make Arabic work in Python corrupts it. That argument has a hole in it: all the evidence was about rendering code. A reversed chart label is embarrassing and fixable. But the same recipe run once, before a file is written, bakes the damage in permanently.

If that ever happened upstream of a training corpus, every model trained on it learned from text no Arabic reader would accept. I could not find anyone who had checked, so I did.

Method, and why the first answer was wrong

I pulled rows for every Arabic dataset I could find on the Hugging Face Hub through the datasets-server API and scanned them with my own linter. 341 datasets.

The first run said 35.6% of an Islamic heritage OCR corpus was corrupted. Two others came back at 27% and 5%. That is a headline. I nearly had one.

Then I printed the characters it was objecting to:

U+FD3F  ﴿  ORNATE RIGHT PARENTHESIS
U+FD3E  ﴾  ORNATE LEFT PARENTHESIS
U+FD40  ﵀  ARABIC LIGATURE RAHIMAHU ALLAAH

Those are not damage. `﴾ ﴿` is how you enclose a Quranic quotation, and `﵀` is an honorific. People type them deliberately. My tool was flagging them because Arabic Presentation Forms-A is interleaved: the positional glyph forms and ordinary semantic characters share one block, and I had excluded only the word ligatures at the end of it.

So on Islamic heritage text, which is a large share of all Arabic corpora, the tool was confidently wrong, in the direction that publicly accuses somebody else's dataset. The fix was to stop tabulating ranges and derive the class from the Unicode character name: a shaping artefact is exactly a codepoint named ... ISOLATED/INITIAL/MEDIAL/FINAL FORM. Everything else in the block is deliberate, and new Unicode additions classify themselves.

All three datasets then read 0%.

The result

276 datasets readable. 26,318 rows. 119,517 text fields.

Twenty-one had any finding at all, and twenty of those were a single stray presentation form in otherwise correct text, most often U+FE91 BEH INITIAL FORM. That is copy-paste out of a PDF, or OCR residue, not the recipe.

I originally wrote that this "splits the word into different tokens". That was wrong, and I have since measured it. What actually happens is worse.

On a tokenizer with no Unicode normalisation step, the word is not split. It is replaced by the unknown token, so every character of meaning is discarded before the model sees anything. One corrupted letter out of four is enough: بيت becomes [UNK].

tokenizerone stray presentation form
aubmindlab/bert-base-arabertv02 (Arabic-specific)6/6 words → [UNK]
google-bert/bert-base-multilingual-cased6/6 words → [UNK]
xlm-roberta-base (SentencePiece)0/6, identical tokens, unaffected
Qwen/Qwen2.5-0.5B (byte-level BPE)survives, but as entirely different tokens

The two Arabic-capable BERTs destroy the word. The multilingual SentencePiece model does not, because it normalises and they do not. If you train Arabic models, that is the whole practical finding: normalise before you tokenise, and a stray glyph costs you nothing instead of costing you the word.

One dataset was the recipe.

One dataset, 100%

Yousefmd/arabic_ocr_dataset — every text field, 1,000 out of 1,000 labels sampled through ten windows across its whole 20,000-row split, 3 to 50 presentation forms per label. A sample row:

ﺑﺪﺃﺕ ﻗﺮﺍﺀﺓ ﺍﻟﺼﻔﺤﺎﺕ ﺍﻷﻭﻟﻰ ﻋﻠﻰ ﻧﻐﻤﺎﺕ

The intended text is بدأت قراءة الصفحات الأولى على نغمات. Every letter has been replaced by its contextual glyph form — INITIAL, MEDIAL, FINAL and ISOLATED all present together. Only that half of the recipe ran: the words are in ordinary logical order, so unicodedata.normalize("NFKC", text) alone gives the line back exactly as written above, with no reversal needed.

It matters more than its size because of which field is damaged. This is an OCR dataset, so text is the ground truth. A model trained on it learns to emit presentation forms instead of plain letters, and then fails string comparison against real Arabic even when it read the image correctly. That is the path from a rendering bug into model weights, observed rather than argued for.

Correction, 13 September 2026. I originally wrote that the words were also "written in visual order", and called that "the complete signature". That was wrong, and it was the more damaging half of what I said, because I said it in a thread on the author's own repository. Only the shaping step ran. Two checks settle it: NFKC alone returns correct readable Arabic, and of 5,423 sampled words carrying more than one presentation form, zero begin with a FINAL form — which is what a word stored in visual order looks like read out of memory. arabic_reshaper.reshape(NFKC(stored)) also reproduces the stored string exactly, for 1,000 of 1,000 labels, which pins down which half ran. The practical consequence is that the repair advice I gave was wrong in the worse direction: I warned that recovery needed more than NFKC because of the lam-alef trap in the post above, when for this data NFKC alone is complete and safe. The detector reports stored glyph forms; visual order is a separate property, and I never measured it — I assumed it, because the two steps are usually copied together. The correction is posted in that thread as well.

One more thing worth writing down, because it nearly put a second wrong number in public. My first check for "is this a presentation form?" tested the Unicode name for ARABIC LETTER … FORM. That silently drops every lam-alef ligature, U+FEF5–FEFC, whose names begin ARABIC LIGATURE instead — the exact character the visual-order test is most sensitive to. Asking the linter's own classifier instead moved the word count from 5,404 to 5,423. The conclusion held, but only because I checked.

Now the honest part: that dataset has 17 downloads. This shows the mechanism reaches training data. It is not evidence that widely-used Arabic corpora are affected, and I am not going to write it as though it were.

What this does not show

65 datasets could not be read, and the viewer fails most often on the largest corpora. So this covers the mid and small end of the distribution well and the head of it poorly. Nothing here reaches web-scale pretraining data. A hundred rows per dataset also cannot find a defect confined to a rare subset.

An earlier version of this audit claimed 121 datasets were unreadable. 61 of those were rate-limit errors caused by my own request rate. A coverage caveat built out of your own impatience is not a caveat, it is a mistake; a slower second pass recovered them.

The thing I would tell anyone doing this

The audit was one command away from being published with three named datasets and a percentage next to each. What stopped it was reading the flagged characters instead of trusting the count.

If you point a measurement tool at somebody else's work, check its output against text you can actually read before the number leaves your machine. A false positive costs them a public accusation and costs you the only thing that makes the measurement worth anything.