Type safety is a double-edged sword (at least in Python)
Don't let type checkers push you to convert loud runtime errors to silent bugs
You've just written (or had written) a function that makes a calculation and saves it to a database:
def calculation(a: int, b: int) -> int:
return a + b
result = calculation(data_a, data_b)
save_result_to_database(result)And being the diligent Python developer you are, you run a type checker against it. And it errors:
error: Argument 1 to "calculation" has incompatible type "int | None"; expected "int"
error: Argument 2 to "calculation" has incompatible type "int | None"; expected "intAnd so you change the function to make the type checker pass:
def calculation(a: int | None, b: int | None) -> int | None:
if a is None or b is None:
return None
return a + b
result = calculation(data_a, data_b)
save_result_to_database(result)This might be The Right Thing. You might want that None value in the database. But from my experience it is much more likely that what you have just done is convert what would have been a loud runtime error to a silent bug.
This is not exactly a win for type safety.
What to do instead?
It's pretty much a mindset thing:
- Get into the mindset that failing loudly can be much better than marching forward silently. Don't let that type checker push you into hiding those runtime errors: it doesn't know the context, but you do.
- If you don't know the context, for example if you're not sure if failing is better than marching foward, then find out. This might mean speaking to people.
- Develop an aversion to complex types, especially
| None. Or at least look at them with some suspicion.
So in the above example, look (maybe far) earlier in the processing than where the type checker is complaining, and deal with the None case there.
if data_a is None or data_b is None:
raise ValueError()
def calculation(a: int, b: int) -> int:
return a + b
result = calculation(data_a, data_b)
save_result_to_database(result)You now have a well-defined validation stage where you explicitly error out if data is not how you expect, you avoid saving bad data to the database, you still have a clear core function that doesn't deal with None, and the type checker is happy 🏆.
In this case the type checker passes because it can interpret the checks against None, but in more complex cases TypeGuard is a great tool that lets you introduce runtime checks that communicate to the type checker.
In summary, type safety is a great tool, but appeasing the type checker with no thought can not only waste time (or tokens 😜), but make your system worse if you do it the wrong way.
