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.
The classic is pulling in json or some other structured data and something not being the type you expect. The error happens way down the line, rather than at import, because why would you do a full validation every time you use JSON right?
I think typehinting is pretty useless in python. Its a nice idea and if your IDE is setup for it, can provide useful information that you're doing something wrong. But its requires lots of tooling and setup to provide useful signal, and is optional.
You’ve never had a bug caused by None showing up in code that didn’t expect it? Or getting an object of the wrong type that didn’t have the method you tried to call on it?