
From code completion to apt install ./tux-im.deb: mypy strict, CI evolution, deb packaging.
This is part 8/8 of the TUX IM Dev Log: From Zero to 0.1 series.
Part 8 · Releasing 0.1.0: mypy strict, CI, deb packaging
Code completion doesn’t mean you can ship. Between the last line of code and apt install ./tux-im_0.1.0-12_all.deb, there were 25 commits doing “engineering” — lint, type checking, CI, deb packaging, version management. This post covers that.
mypy strict: How 0 errors were forced
Commit 153587c mypy strict: 0 errors was a milestone — the entire project (27 Python files) passed mypy --strict.
What mypy strict includes:
- All function parameters and return values must have type annotations.
- No
Anytype escaping. - No implicit
Optional. - No abuse of
# type: ignore.
Why is it worth it?
- Refactoring without fear — change a function signature, mypy immediately lists all call sites.
- Fast onboarding — IDE auto-completion is accurate, no need to read entire files to guess types.
- Runtime type bugs nearly vanish —
AttributeError: 'NoneType' object has no attribute 'x'— that classic Python error is essentially impossible to write under strict mode.
Notable fixes:
- config.dict → config.dictionary — avoid name collision with Python’s built-in
dicttype.- shortcut handler type annotations — previously
Callablewith no parameter info, mypy couldn’t see it.- PEP 563 annotation parsing — after
from __future__ import annotations, generics likelist[int]in dataclass fields neededtyping.List[int]before Python 3.9.- ASR module gets
ignore_errorsas a whole — ctypes cfunc signatures are incompatible with mypy strict. A pragmatic compromise.
How the lint debt was paid off
The project historically had 53 ruff errors. The approach wasn’t a single commit fix, but a three-step process:
74ee0af style: manual lint fixes for the non-fixable 53 ruff errors— manually fixed what couldn’t be auto-fixed.f36218d style: auto-fix ruff lint debt—ruff check --fixauto-fixed.2d5daf6 style: rename _MAX_PINYIN_LEN to _max_pinyin_len per ruff N806— renamed constants per N806 rules.
After that, CI enforces ruff check . on every push — no new debt accumulates.
CI: Evolution of GitHub Actions
Starting from d88808a ci: add GH Actions workflow, the CI platform was built step by step. Each step had its pitfalls.
Pitfall 1: respx is a PyPI package, not apt

The first time I wrote the CI, I mistakenly tried to install respx via apt. respx is a PyPI package used for HTTP mocking in tests. This taught me to carefully distinguish between apt and pip dependencies in CI workflows.
Pitfall 2: The deb packaging learning curve
Building a proper .deb package required understanding Debian’s package metadata, control files, and the dpkg-deb toolchain. The key was getting the debian/control, debian/changelog, and debian/rules files right. The final package tux-im_0.1.0-12_all.deb could be installed with a simple apt install ./tux-im.deb.
Pitfall 3: Version numbering discipline
Adopting SemVer with a Debian-compatible version scheme (0.1.0-12 where 12 is the Debian revision number) required careful management. Each build needed an updated changelog entry.
Summary
The engineering work — lint, types, CI, packaging — was invisible to end users but essential for maintainability. The 25 commits between “code works” and “code ships” are the difference between a prototype and a product. Key takeaways:
- mypy strict from day one (or at least before the codebase grows too large)
- CI that runs on every push prevents regression debt
- Proper packaging makes installation trivial for users
- Version discipline enables reproducible builds
“Code is only half the story. The other half is making it shippable.”
Comments