Skip to content
forzagreenPublic

About

Arabic verb conjugator — every form of any verb from its root and pattern; a pure-Python port of Wiktionary's Module:ar-verb, verified against 16,593 printed forms

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

22 Commits

Folders and files

Repository files navigation

mazini

PyPI CI

Arabic verb conjugator for Python. Give it a root and a pattern (وزن) and it returns every form of the verb: the past, the three non-past moods, the imperative, active and passive, in all thirteen persons, plus the participles and the مصدر — 119 slots, fully vocalised, with the attested alternative spellings where Arabic has them.

It is a pure-Python port of Arabic Wiktionary's conjugation engine, وحدة:ar-verb (itself descended from Module:ar-verb on English Wiktionary), and it is verified two ways:

  • Against the book. Every one of the 16,593 printed forms of the 495 paradigms in أنطوان الدحداح, معجم تصريف الأفعال العربية — the reference work on the subject, transcribed and published as the ar-conjugation dataset — is among the forms the engine generates.

  • Against the original. For 7,884 inputs (every root × pattern combination of a 154-root grid, plain, passive and assimilated, plus every book verb) the output is byte-identical to the Lua module's: 744,356 forms, footnotes and metadata fields, and the same 201 rejected inputs.

  • Pure Python, zero dependencies, no data files. Python 3.11+. Typed.

Named after أبو عثمان المازني (Abū ʿUthmān al-Māzinī, d. 863), whose كتاب التصريف is the first standalone treatise on Arabic morphology.

A JavaScript/TypeScript port with the same API and the same tests, and a live demo: mazini-js — forzagreen.github.io/mazini-js.

Install

pip install mazini

Usage

from mazini import conjugate

v = conjugate("كتب", "فعَل يفعُل")
v.slots["past_3ms"][0].form  # "كَتَبَ"
v.slots["ind_3ms"][0].form  # "يَكْتُبُ"
v.slots["imp_2fs"][0].form  # "اُكْتُبِي"
v.slots["ap"][0].form  # "كَاتِب"
v.verb_type  # "فعل ثلاثي مُجرَّد صحيح سالم"

# A pattern can be given by its Arabic name or its form code; both take the same options.
conjugate("قول", "أفعل").slots["past_3ms"][0].form  # "أَقَالَ"
conjugate("علم", "V").slots["vn"][0].form  # "تَعَلُّم"
conjugate("كتب", "I", vowels=("a", "u"))  # same as "فعَل يفعُل"

# Two switches, the same ones {{تصريف}} takes on the wiki:
conjugate("أخذ", "افتعل", reduced=True).slots["past_3ms"][0].form  # "اِتَّخَذَ" (assimilated affix)
conjugate("علم", "فعِل يفعَل", passive=True)  # a full personal passive for a verb the engine would default to impersonal
conjugate("جلس", "فعَل يفعِل", passive="ipass")  # impersonal (جُلِسَ عَلَيْهِ only); also "pass", "nopass"

# The assimilated shape exists only for some roots; the engine, like the wiki, applies it to any:
from mazini import reducible

reducible("سمع", "تفعّل")  # True: اِسَّمَّعَ
reducible("كتب", "تفعّل")  # False: no *اِكَّتَّبَ

Without passive, the engine guesses the passive from the pattern, and the guess is often wrong for form I (it gives جلس a full passive, *جُلِسْتُ). For a listed verb, lexicon_passive (below) gives it from the verb's transitivity instead.

The result's slots maps a slot name (past_3ms, ind_pass_2fp, juss_1p, imp_2fp, ap, pp, vn, …) to the list of forms for that slot, each Form(form, footnotes); a slot the verb lacks is absent. Alongside it: lemma, verb_form, wazn, radicals, weakness, passive (the passive type), irregular, morph_patterns, has_active / has_passive, uncertain_slots (slots shown as "?", i.e. a form-I مصدر or participle the root alone cannot predict), verb_type and classification.

Roots may be written كتب, ك ت ب or ك_ت_ب; hamza seats fold to ء and ى to ي. Bad input raises MaziniError, whose .code is stable (bad_root, unknown_wazn, reduced_not_applicable, bad_option, …).

from mazini import WAZNS, SLOTS, classify_triliteral_verb, loose

WAZNS  # the 22 patterns: name, form code, vowels, مجرد/مزيد class
classify_triliteral_verb("ق", "و", "ل")  # "معتل أجوف واوي"
loose("فَعَلَا") == loose("فَعَلا")  # the comparison the book tests use

Command line:

mazini كتب "فعَل يفعُل"            # the table
mazini أخذ افتعل --reduced --json  # everything, as JSON
mazini كتب                        # the root's attested verbs, from the lexicon below
mazini --list-wazn

Is it a verb? The attested-verb lexicon

The engine conjugates any root on any pattern, and most of those combinations are not words. The mazini.lexicon module says which are, and whether each is transitive, from the 24,188 verbs over 7,626 roots of جداول إحصاء الأفعال العربية (Marwan al-Bawwab and Yahya Mir Alam, CC BY 4.0):

from mazini.lexicon import LEXICON_INFO, attested_wazns, is_attested, lexicon_passive, lexicon_roots, transitivity

is_attested("كتب", "فعَل يفعُل")  # True
is_attested("كتب", "افعوعل")  # False
attested_wazns("كتب")  # ['فعَل يفعُل', 'فعَل يفعِل', 'فعّل', 'فاعل', 'أفعل', 'تفعّل', 'تفاعل', 'افتعل', 'استفعل']
transitivity("جلس", "فعَل يفعِل")  # "intransitive" (ل); also "transitive" (م), "both" (ك), None if not listed
lexicon_passive("جلس", "فعَل يفعِل")  # "ipass": an intransitive verb's passive is impersonal
lexicon_passive("فهم", "فعِل يفعَل")  # "pass": a transitive one's is full, فُهِمْتُ
conjugate("جلس", "فعَل يفعِل", passive=lexicon_passive("جلس", "فعَل يفعِل"))
len(lexicon_roots())  # 7626
LEXICON_INFO  # source_url, source_commit, source_date, roots, verbs

Roots and patterns are written as for conjugate. The data is 52 KB of strings decoded on first use (a few milliseconds); nothing else in the package imports it, and conjugate is never restricted by it: no list of a living language is complete, so an absence means "not listed", not "not a verb".

What it does not do (yet)

It conjugates from a root and a pattern, which is what every caller on Arabic Wiktionary does. The Lua module can also take a written, vocalised verb and infer its root (infer_radicals), accept per-slot overrides, and render the wikitext table; none of that is ported. The subjunctive, jussive, participles and مصدر are generated but are outside the book's tables, so they are verified only against the Lua module, not against print.

Development

uv sync
uv run pytest -q          # hamza, book, golden, loose, api
uv run ruff check
python3 tools/gen_golden.py --ar-verb DIR   # regenerate tests/fixtures/ from the module's source checkout (needs luajit)
python3 tools/gen_lexicon.py --source DIR   # regenerate src/mazini/_lexicon_data.py from a checkout of arabic-morphology

tests/fixtures/ is generated by tools/port_fixtures.py in the private repository that maintains the Lua module and the book transcription: golden.jsonl.gz (the module's output for 7,884 inputs), hamza.jsonl.gz (every hamza-seating call it made), loose.jsonl.gz (the normaliser's parity set), book/ (the 495 paradigms as CSV) and MANIFEST.json (the upstream commit). The source is ported file for file from mazini-js, which was ported from the Lua; the three keep the same function names and the Lua line references.

Licence

© 2026 Wael Tellat. Released under CC BY 4.0; see LICENSE.

About

Arabic verb conjugator — every form of any verb from its root and pattern; a pure-Python port of Wiktionary's Module:ar-verb, verified against 16,593 printed forms

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages