Skip to content

Type checking, or writing tests? Why not both!

Ever since static and dynamic typed languages have existed, there has existed an argument between them, one that argues what's better: type checking, or writing tests.

The argument essentially boils down to these two sides: The dynamic side would say something like, "I've been writing python for so long without types, why should I care now?", or, "I don't need types in my code because I write tests". Conversely, people can think they don't need tests (at least not as much as dynamic languages) because their type checker catches their bugs for them.

The truth is that both of these arguments are extreme, and this, both are wrong. The correct answer lies somewhere between them. I think both static types and tests are equally necessary in a large codebase, because they both serve a different purpose.

Why types

Type checking eliminate a whole class of bugs from your codebase: type errors.

Writing good tests is hard, especially in a dynamically typed language. It's very easy to write tests that forget to check all the possible invalid types that you could pass to the function. For example:

def process(items):
    for item in items:
        print("Processing", item.value.id)

It is very easy to write 5 different passing tests for this code, but forget to write a test where value is None. If it is None, you'll get an attribute error raised at runtime.

If the schema of each item object was well defined beforehand, the problem would've easily been caught beforehand:

from dataclasses import dataclass
from typing import Iterable

@dataclass
class ItemDetails:
    id: int
    name: str

@dataclass
class Item:
    value: ItemDetails | None
    weight: int

def process(items: Iterable[Item]) -> None:
    for item in items:
        print("Processing", item.value.id)

Now mypy will catch that problem for you. Same goes for if someone tries to pass something completely wrong to process, like a dictionary. Mypy has taken the burden of handling completely invalid usage of your code away from you, so that you can focus on the fine details.

Why tests

Tests are important, because they are necessary for checking your logic. Logical errors can't be possibly tested through type checking. For example, here's a perfectly type checked code:

def sum(a: int, b: int) -> int:
    return (a + b) // 2

This is a really obvious example, but real world scenarios can be much more complicated than this. You can't expect a code analysis tool to figure out problems like deadlocks, missing else-clauses, incomplete or invalid logic, etc. Thus, testing your code and all its possible outputs is an essential part of developing a big project.

Both type checkers and tests work in unison: Type checking takes away the task of identifying and fixing inputs to your code that are obviously invalid, such as passing a str instead of int. Tests on the other hand, prevent more fine grained bugs, that can come from logical errors and the like.

So my advice would be to use both tests and types -- so that you can be completely confident in your code's correctness.