Rust port of the textstat textual analysis library, with Python bindings. Drop-in replacements for 11 key metrics in British and US English.
99.98% exact output match, with at least 2.4x-7x speedup, with 2 most sped-up metrics at ~14x and ~79x based on benchmarking scripts included in repo, running on Wikipedia dataset as a varied, modern text corpus.
pip install textstat-rsPre-release versions need pip install --pre textstat-rs.
Requires Python 3.10+. Wheels are built for Linux (x86_64, x86, aarch64, armv7; glibc and musl), macOS (x86_64, arm64) and Windows (x64, arm64). They are abi3 wheels, so one wheel per platform covers every supported Python version. On other platforms pip falls back to the sdist, which needs a Rust toolchain to build.
Import name is textstat_rs; every metric takes the text as its first argument.
import textstat_rs
text = "The cat sat on the mat. It was a sunny day."
textstat_rs.flesch_reading_ease(text) # 108.96159090909092
textstat_rs.flesch_kincaid_grade(text) # -0.5722727272727273
textstat_rs.text_standard(text) # '0th and 1st grade'The exposed metrics match textstat's signatures, so an existing import can be swapped in place:
import textstat_rs as textstatOptional arguments are the same as the Python library, other than the exceptions noted below:
textstat_rs.gunning_fog(text, syllable_threshold=3)
textstat_rs.linsear_write_formula(text, strict_lower=False, strict_upper=True)
textstat_rs.reading_time(text, ms_per_char=14.69)The counting helpers the formulas are built on are exposed too: syllable_count, sentence_count, lexicon_count, char_count, letter_count, polysyllabcount, miniword_count and difficult_words.
British and American English are both supported. The default is en_US, matching textstat.
Set it once, the way textstat does:
textstat_rs.set_lang("en_GB")
textstat_rs.get_lang() # 'en_GB'
textstat_rs.syllable_count("colourful") # 3 (2 under en_US)supported_langs() returns canonical tags, to allow enumerating locales rather than hardcoding them:
textstat_rs.supported_langs() # ['en_US', 'en_GB']You can also set the locale per call. It is keyword-only, and overrides over the default:
textstat_rs.flesch_reading_ease(text, lang="en_GB")lang= is offered only where the locale can change the answer. It reaches exactly one decision — which hyphenation dictionary spells out syllables for words missing from CMUdict — so char_count, lexicon_count, sentence_count, letter_count, miniword_count, reading_time, mcalpine_eflaw, coleman_liau_index and automated_readability_index do not take it, because it would do nothing.
Tags are resolved the way pyphen does — lowercased, - normalised to _, then trailing subtags dropped until something matches — so en-GB, EN_GB and en_US_posix all work.
Four differences from textstat worth knowing:
- Only English is supported.
textstatwould accept"fr"and reach for French coefficients and a French dictionary. textstat-rs vendors only the two English hyphenation dictionaries, so anything that doesn't resolve toen_USoren_GBraisesValueError. - Bad locales fail immediately.
textstat'sset_langis a bare assignment, so a typo is accepted and only surfaces later as aKeyErrorfrom insidepyphen— and only once some word actually misses CMUdict, which for a short text may be never. - Bare
"en"means British.pyphenregisters a short name for the first matching file in sorted order, andhyph_en_GB.dicsorts beforehyph_en_US.dic. Kept deliberately, soenanden_AUget the British dictionary. The default when nothing is set is stillen_US. lang=is honoured.textstatstill acceptssyllable_count(text, lang=...)but warns and discards it. Here it works.
| Python function | Exposed? | Notes |
|---|---|---|
flesch_reading_ease |
✅ | |
flesch_kincaid_grade |
✅ | |
smog_index |
✅ | |
coleman_liau_index |
✅ | |
automated_readability_index |
✅ | |
dale_chall_readability_score |
✅ | |
linsear_write_formula |
✅ | signature parity (strict_lower, strict_upper) |
gunning_fog |
✅ | Rust exposes syllable_threshold arg, Python hardcodes it |
spache_readability |
✅ | Rust missing float_output arg |
text_standard |
✅ | Rust missing float_output arg, returns String only |
reading_time |
✅ | |
mcalpine_eflaw |
✅ | |
dale_chall_readability_score_v2 |
❌ | |
lix |
❌ | |
rix |
❌ | |
fernandez_huerta |
❌ | Spanish |
szigriszt_pazos |
❌ | Spanish |
gutierrez_polini |
❌ | Spanish |
crawford |
❌ | Spanish |
gulpease_index |
❌ | Italian |
wiener_sachtextformel |
❌ | German |
osman |
❌ | Arabic |
Scores are compared against textstat over 1000 Wikipedia articles (up to 5000 chars each), across en_US and en_GB: 99.98% of 22000 comparisons match exactly, and the largest disagreement anywhere is 0.049566.
| locale | comparisons | exact match | max delta |
|---|---|---|---|
| en_US | 11000 | 99.98% | 0.049566 |
| en_GB | 11000 | 99.97% | 0.049566 |
Metrics returning a grade band rather than a number are compared as exact strings: text_standard matches exactly 1000/1000 in en_US, 1000/1000 in en_GB.
Per metric (en_US)
| metric | avg delta | max delta |
|---|---|---|
| flesch_reading_ease | 0.000000 | 0.000000 |
| flesch_kincaid_grade | 0.000000 | 0.000000 |
| automated_readability_index | 0.000000 | 0.000000 |
| coleman_liau_index | 0.000000 | 0.000000 |
| dale_chall_readability_score | 0.000000 | 0.000000 |
| gunning_fog | 0.000050 | 0.049566 |
| smog_index | 0.000000 | 0.000000 |
| linsear_write_formula | 0.000000 | 0.000000 |
| mcalpine_eflaw | 0.000000 | 0.000000 |
| spache_readability | 0.000011 | 0.010657 |
| reading_time | 0.000000 | 0.000000 |
Generated from dc1a373 on 2026-08-24, python 3.12.4 vs textstat 0.7.13.
One call per metric over 100 concatenated Wikipedia articles (0.46 MB), median of 5. Both libraries are warmed first, so these are steady-state numbers with lazy resource loading excluded.
| metric | textstat-rs (ms) | textstat (ms) | speedup |
|---|---|---|---|
| flesch_reading_ease | 39.29 | 149.60 | 3.81x |
| flesch_kincaid_grade | 36.45 | 148.43 | 4.07x |
| automated_readability_index | 18.87 | 51.10 | 2.71x |
| coleman_liau_index | 13.68 | 59.91 | 4.38x |
| dale_chall_readability_score | 28.11 | 186.25 | 6.63x |
| gunning_fog | 28.72 | 187.38 | 6.52x |
| smog_index | 27.78 | 207.86 | 7.48x |
| linsear_write_formula | 0.04 | 4.16 | 107.98x |
| mcalpine_eflaw | 18.61 | 42.15 | 2.26x |
| spache_readability | 27.91 | 193.68 | 6.94x |
| reading_time | 0.79 | 12.80 | 16.27x |
On top of that, the first call in a fresh process pays a one-off load of the syllable and word-list resources: up to 34 ms for textstat-rs against 361 ms for textstat.
Generated from dc1a373 on 2026-08-24, python 3.12.4 vs textstat 0.7.13.
The scoring resources are compiled into the extension by include_str!, so
they ship inside every wheel. Their notices ship with them, under
textstat_rs-<version>.dist-info/licenses/.
| Data | Source | Licence | Notice |
|---|---|---|---|
cmudict.txt |
CMU Pronouncing Dictionary | BSD-2-Clause-style | src/data/cmudict-LICENSE.txt |
hyph_en_US.dic |
pyphen / LibreOffice, from hyphen.tex |
BSD-style | src/data/README_hyph_en_US.txt |
hyph_en_GB.dic |
pyphen / LibreOffice, from ukhyphen.tex |
BSD-style | src/data/README_hyph_en_GB.txt |
easy_words.txt |
textstat (Dale-Chall list) | MIT | covered by textstat's MIT licence |
Both .dic files are vendored byte-for-byte from pyphen, and the notice files
keep their upstream names so they can be diffed against it directly. The
pyphen-LICENSE.txt note covers pyphen itself (GPL 2.0+/LGPL 2.1+/MPL 1.1
tri-licence). None of pyphen's code is used here, only the two dictionaries,
which carry the BSD-style terms above.
Only the data is third-party. The Rust and Python code in this repository is MIT, per LICENSE.