Re:Show HN: Gawain – Require type hints on all Python variables
by rented_mule on Sunday November 15, 2026 @07:40AM
(#50506521)
I've been writing Python since v1.4 and no other language comes close to matching my brain so well. I've rarely hit such bugs, but I hit a gigantic source of them at one job.
It was a 1-2M line codebase built in the Python 2.3-2.5 years. A pattern that had evolved in that codebase was to pass around a context dict to every non-trivial call. It was always the first parameter (other than `self`) and always had the same name. The only way to know the contents of the dict was to know the upstream call path to your function (not just the stack, any detours could have written into it as well). Things like a "price" whose value might be an integer number of US pennies or a Money object with a value and a currency identifier (e.g., USD, JPY, etc.). Much of the code just assumed it was whatever it was the first time they inspected the context dict as it arrived at their call site. Someone else modifies a code path and suddenly the contents of the context dict change on you. If your automated tests don't include that new code path, you catch the issue in prod. So, automated tests had to include all possible run-time call paths to catch these bugs before deployment, and that's impractical.
To me, one of the most important advancements Python has ever made is frozen, kw_only dataclasses with thorough type hints. They bring a lot of clarity to the code that uses them, such code is easier to test, and there are fewer bugs discovered at run-time. Extensive use of them makes type inference easier, so hints can me skipped in many places. Any difficulty in creating them often means the data model is not sufficiently flushed out, and that's good to know. I rarely write non-dataclass classes in recent years. I do add read-only properties to dataclasses liberally.
All this fixes many of the challenges that come with Python's highly dynamic nature, but let us keep many of the benefits.
It was a 1-2M line codebase built in the Python 2.3-2.5 years. A pattern that had evolved in that codebase was to pass around a context dict to every non-trivial call. It was always the first parameter (other than `self`) and always had the same name. The only way to know the contents of the dict was to know the upstream call path to your function (not just the stack, any detours could have written into it as well). Things like a "price" whose value might be an integer number of US pennies or a Money object with a value and a currency identifier (e.g., USD, JPY, etc.). Much of the code just assumed it was whatever it was the first time they inspected the context dict as it arrived at their call site. Someone else modifies a code path and suddenly the contents of the context dict change on you. If your automated tests don't include that new code path, you catch the issue in prod. So, automated tests had to include all possible run-time call paths to catch these bugs before deployment, and that's impractical.
To me, one of the most important advancements Python has ever made is frozen, kw_only dataclasses with thorough type hints. They bring a lot of clarity to the code that uses them, such code is easier to test, and there are fewer bugs discovered at run-time. Extensive use of them makes type inference easier, so hints can me skipped in many places. Any difficulty in creating them often means the data model is not sufficiently flushed out, and that's good to know. I rarely write non-dataclass classes in recent years. I do add read-only properties to dataclasses liberally.
All this fixes many of the challenges that come with Python's highly dynamic nature, but let us keep many of the benefits.