From chardet¶
chardet is the pure-Python universal character encoding detector, the tool most
projects reach for when they receive undeclared bytes from a file, a socket, or an HTTP body with no reliable
Content-Type. chardet.detect(data) returns an {"encoding", "confidence", "language"} dict, detect_all
ranks the candidates, and UniversalDetector accumulates a stream chunk by chunk so a reader can stop as soon as the
verdict is settled. It ships an ensemble of probers (escape-sequence, multi-byte, and single-byte frequency models) that
vote on the most likely encoding, an approach inherited from Mozilla’s original universalchardet. It is a dependency
of requests and countless scrapers and ETL pipelines. This guide also covers cchardet, whose cchardet.detect is the same call bound to the C uchardet engine.
turbohtml covers the same ground with turbohtml.detect.detect(), a standalone entry point over the C detector it
already ships for parse(). It answers “what encoding are these bytes?” without an HTML parser in the
call path, and returns a typed EncodingMatch in place of chardet’s dict.
turbohtml vs chardet¶
Dimension |
turbohtml |
chardet |
|---|---|---|
Scope |
WHATWG-conformant detection: a byte-order-mark sniff, an HTML |
Ensemble of probers voting on the most likely encoding; ignores HTML markup and never applies a browser’s windows-1252 fallback. |
Feature breadth |
|
|
Performance |
ASCII, valid UTF-8, and real web pages short-circuit before any scoring, resolving 50x to 2000x ahead; legacy single-byte text runs about 3x ahead. See the table below. |
Prober ensemble runs every model on every input; no fast path for clean UTF-8 or ASCII. |
Typing |
Fully typed: |
Returns untyped |
Dependencies |
None beyond the turbohtml C extension. |
Pure Python, no dependencies ( |
Maintenance |
Actively developed alongside the parser; the detector is the same code the parser uses in production. |
Maintained but slow-moving; the model set has been stable for years. |
Feature overlap¶
The detection surface ports one-to-one:
chardet.detect(data)->turbohtml.detect.detect(), same three fields as a typed record.chardet.detect_all(data)->turbohtml.detect.detect_all(), ranked candidates best first.UniversalDetector()withfeed/close/reset/done/result->turbohtml.detect.EncodingDetectorwith the same five members.UniversalDetector(lang_filter=...)-> aDetectionallowedfrozenset of the WHATWG encoding names to keep.chardet’s implicit 0.2 minimum confidence ->
Detection.chardet().cchardet.detect(data)(including the maintainedfaust-cchardetfork) ->turbohtml.detect.detect().
What turbohtml adds¶
HTML
<meta>charset awareness: a declaration in the first 1024 bytes wins per the WHATWG prescan, so detection agrees with what the browser andturbohtml.parse()would decode. chardet and cchardet ignore markup entirely.Browser-faithful results: turbohtml reports the WHATWG canonical encoding a browser would pick, where chardet often names a sibling or superset (ISO-8859-7 for Greek windows-1253 bytes, GB18030 for GBK).
A frozen
Detectionconfig with a language hint, anallowedset, and a mutually exclusiveexcludedset – richer than chardet’s singlelang_filter.Fully typed results:
EncodingMatchinstead of an untyped dict.One detection path shared with parsing: standalone
detectandparse(detect_encoding=True)always agree on the same bytes.
What chardet has that turbohtml does not¶
A wider candidate set. turbohtml scores chardetng’s list: UTF-8, ISO-2022-JP, five CJK encodings, and 19 single-byte encodings. chardet’s extras outside that set (UTF-16/32 without a byte-order mark, MacCyrillic, TIS-620, Johab) resolve to the closest WHATWG candidate instead. No equivalent when you need one of those exact labels. A UTF-16 or UTF-32 stream that does carry a mark now reports its exact label (see below).
A raw CJK speed edge on the C fork. On CJK-heavy byte streams (the Shift_JIS row),
cchardet’s uchardet engine outruns turbohtml, whose CJK scoring drives a CPython incremental codec per candidate. Workaround: keepfaust-cchardetfor that one workload if it dominates; turbohtml leads on every other row.
Performance¶
detect a byte stream’s encoding |
turbohtml |
|
|---|---|---|
ascii (1 kB) |
1.89 µs |
118 µs (62.6x) |
utf-8 russian (4 kB) |
3.64 µs |
150 µs (41.3x) |
windows-1251 russian (4 kB) |
185 µs |
536 µs (2.9x) |
windows-1252 french (4 kB) |
186 µs |
674 µs (3.7x) |
shift_jis japanese (4 kB) |
708 µs |
547 µs (0.8x) |
utf-8 page (95 kB) |
642 ns |
1.27 ms (1979x) |
Certain input short-circuits before any scoring, so ASCII, valid UTF-8, and real web pages resolve 50x to 2000x ahead of
chardet’s prober ensemble; declaration-less legacy single-byte text still runs about 3x ahead. Both libraries decode a
15-sample multilingual differential correctly, though chardet often names a sibling or superset where turbohtml reports
the WHATWG encoding a browser would pick. The one exception is CJK-heavy bytes, where cchardet’s uchardet engine
leads (the Shift_JIS row, 22x); turbohtml leads on the other rows.
How to migrate¶
Swap the import and read the fields off the typed record instead of the dict:
turbohtml |
|
|---|---|
|
|
|
|
|
|
|
|
the implicit 0.2 minimum confidence |
|
|
The dict becomes a typed EncodingMatch with the same three fields:
# chardet
import chardet
guess = chardet.detect(data) # {"encoding": ..., "confidence": ..., "language": ...}
# turbohtml
from turbohtml.detect import detect
match = detect(data) # EncodingMatch(encoding=..., confidence=..., language=...)
from turbohtml.detect import detect
match = detect("Привет мир, как дела".encode("cp1251"))
print(match.encoding, match.language)
windows-1251 Russian
The maintained cchardet story is a footnote: the original package stopped at 2.1.7 (2021) and no longer compiles on
Python 3.11+, where the longintrepr.h header it includes left the public C API. The fork faust-cchardet keeps the import cchardet name alive. Both expose only detect
and a UniversalDetector without detect_all, and neither reports a language; the turbohtml calls above replace
either package unchanged.
Gotchas and pitfalls¶
Encoding names differ in spelling, not identity: turbohtml reports the WHATWG canonical name (
windows-1251,Shift_JIS), chardet its own casing (Windows-1251,CP932), cchardet upper case. Every name turbohtml can detect is a validcodecsalias, sodata.decode(match.encoding)works; only a<meta>-declaredx-user-definedhas no stdlib codec.Confidence scales are not comparable across libraries. turbohtml’s confidence is the candidate’s share of the positive frequency scores (1.0 for a declaration or structural proof, 0.0 for the no-evidence windows-1252 fallback); do not port a chardet threshold number directly, use
Detection.chardet()for its 0.2 floor.turbohtml honors an HTML
<meta>charset declaration in the first 1024 bytes, per the WHATWG prescan; chardet and cchardet ignore markup. FeedDetectionexcludedconstraints instead of re-sniffing when a declaration is known to lie.A byte-order mark reports the mark’s own label and sets
EncodingMatch.bom: a UTF-8 mark comes back asUTF-8-SIG(chardet’s spelling), and the UTF-16 and UTF-32 marks asUTF-16LE/UTF-16BE/UTF-32LE/UTF-32BE. Decode with the matching codec (utf-8-sig,utf-16,utf-32) to strip the mark. The spec-lockedparse()sniff is unaffected – it keeps the plain WHATWG name and treatsFF FE 00 00as UTF-16LE, sodetectandparse(detect_encoding=True)agree on every input except a marked one.The candidate set is chardetng’s: UTF-8, ISO-2022-JP, five CJK encodings, and 19 single-byte encodings. chardet’s extras outside that set (UTF-16/32 without a mark, MacCyrillic, TIS-620, Johab) resolve to the closest WHATWG candidate instead.