Ship type information (PEP 561) - #71
Merged
Merged
Conversation
faust-cchardet is a drop-in replacement for chardet, and chardet ships
py.typed with a fully annotated detect(). So switching to this package
currently *downgrades* a user's type coverage: mypy reports
error: Skipping analyzing "cchardet": module is installed, but
missing library stubs or py.typed marker [import-untyped]
and every call becomes Any. All of these pass silently today:
cchardet.detect(raw)["encoding"].upper() # None-deref at runtime
cchardet.detect(raw)["confidence"] # float used as int
cchardet.detect(raw)["encodng"] # typo -> KeyError
cchardet.detect("a str, not bytes") # TypeError at runtime
With this change mypy catches all four, the typo with a "Did you mean
encoding?" suggestion.
Adds a py.typed marker, a DetectionResult TypedDict describing what
detect() and UniversalDetector.result actually return, annotations across
the public API and the CLI, and _cchardet.pyi -- without a stub the
compiled extension is opaque and the package degrades to Any even with
py.typed present. Both files are installed via py.install_sources, and
src/tests/test_typing.py guards that, since dropping them from that list
fails silently.
Deliberately NOT adding a mypy CI gate. The shipped Python surface is 148
lines; mypy finds zero real bugs in it and --strict finds only
missing-annotation boilerplate. All the substantive logic is Cython and
C++, which mypy cannot see. The value here is entirely outward-facing.
Two documentation bugs fixed in passing, both surfaced by writing the
types down:
- detect()'s docstring claimed `msg: str`, but passing a str raises
TypeError: expected bytes. It takes bytes.
- detect() guarded `isinstance(msg, (bytes, bytearray))` when picking the
BOM prefix, implying bytearray support. The extension signature is
`bytes msg`, so bytearray and memoryview both raise TypeError before
that line is reached -- the fallback branch was unreachable.
__enter__ uses a bound TypeVar rather than typing.Self, which is 3.11+;
this package supports 3.10. Verified on 3.10: mypy clean in default and
--strict mode, 134 passed, and the built wheel contains both py.typed and
_cchardet.pyi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TWqKLkfwd8fPxhU4KjXUVB
wbarnha
pushed a commit
that referenced
this pull request
Aug 7, 2026
#71 (PEP 561 type information) merged to master while this branch was open. It landed without a changelog entry, deliberately: this branch is what renames the stale "3.1.0 (unreleased)" header and opens the 3.2.0 section, so adding one there beforehand would have guaranteed a conflict. Adding it here now that the two are merged, so the 3.2.0 release notes cover everything that will actually be in 3.2.0. Verified the two changes coexist: free-threaded 3.13t build keeps the GIL disabled, reports version 3.2.0, and the combined suite is 138 passed / 1 skipped (up from 134 -- the four typing tests came with the merge). mypy is clean on the merged tree, so the critical_section decorators this branch adds do not invalidate the _cchardet.pyi stub from #71. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TWqKLkfwd8fPxhU4KjXUVB
wbarnha
pushed a commit
that referenced
this pull request
Aug 8, 2026
#71 (PEP 561 type information) merged to master while this branch was open. It landed without a changelog entry, deliberately: this branch is what renames the stale "3.1.0 (unreleased)" header and opens the 3.2.0 section, so adding one there beforehand would have guaranteed a conflict. Adding it here now that the two are merged, so the 3.2.0 release notes cover everything that will actually be in 3.2.0. Verified the two changes coexist: free-threaded 3.13t build keeps the GIL disabled, reports version 3.2.0, and the combined suite is 138 passed / 1 skipped (up from 134 -- the four typing tests came with the merge). mypy is clean on the merged tree, so the critical_section decorators this branch adds do not invalidate the _cchardet.pyi stub from #71. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TWqKLkfwd8fPxhU4KjXUVB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Independent of the #55 stack (#68 / #69 / #70) — this branches from
masterand touches no shared lines, so it can merge in any order.Why
faust-cchardet is a drop-in replacement for
chardet, and chardet shipspy.typedwith a fully annotateddetect(). So switching to this package currently downgrades a user's type coverage:Every call becomes
Any. All four of these pass silently today, and all four are caught after this change:The typo even gets
note: Did you mean "encoding"?.What's in it
py.typedmarker, and aDetectionResultTypedDict describing whatdetect()andUniversalDetector.resultactually return — including that both members areNonewhen nothing is detected, which is the case callers most often forget._cchardet.pyi— without a stub the compiled extension is opaque and the package degrades toAnyeven withpy.typedpresent.py.install_sources, andsrc/tests/test_typing.pyguards that, because dropping them from that list fails silently — no error anywhere, downstream just quietly loses typing again.What's deliberately not in it
No mypy CI gate. The shipped Python surface is 148 lines; mypy finds zero real bugs in it and
--strictfinds only missing-annotation boilerplate. All the substantive logic is Cython and C++, which mypy cannot see. Adding a gate would also mean adding a second unenforced linter next toruff, which is already inrequirements-dev.lock, isn't wired to CI, and reports 349 baseline errors. The value here is entirely outward-facing, so that's where the change is.Two documentation bugs fixed in passing
Both surfaced purely by writing the types down:
detect()'s docstring claimedmsg: str, but passing astrraisesTypeError: expected bytes. It takesbytes.detect()guardedisinstance(msg, (bytes, bytearray))when picking the BOM prefix, implying bytearray support. The extension signature isbytes msg, sobytearrayandmemoryviewboth raiseTypeErrorbefore that line is reached — the fallback branch was unreachable. If accepting the buffer protocol is wanted, that's a separate feature.Verification
On Python 3.10 (the floor, since
DetectionResultuses PEP 604 unions):mypy src/cchardet/mypy --strict src/cchardet/cchardet/py.typedandcchardet/_cchardet.pyi__enter__uses a bound TypeVar rather thantyping.Self, which is 3.11+. Ruff'sPYI019would preferSelf; that's the one lint code this branch adds over themasterbaseline for this directory, and it's not applicable at a 3.10 floor. The__all__addition incidentally fixes a pre-existingF401.Generated by Claude Code