Unit testing cannot prove the absence of bugs and shit happens in spite of their presence. That's why I still value (good) static type systems. But besides a good static type system that can prove certain properties about the code (and I'm not talking about Java's type system here), you can also do design by contract [1], which is precisely what @lkrubner is doing and I find that to be pretty cool.
Well, it would be cooler if the compiler could check those contracts at compile-time and issue some helpful warnings, but at runtime they are still valuable because the code will fail sooner rather than later. Plus you can probably disable them completely, should you experience problems with performance in production.
They also serve as documentation for other developers, documentation that you're forced to keep in sync. This documentation is not about the actual business logic, that ends up being laid out in tests, but rather about interface specifications and invariants.
So there you have it - testing serves a different purpose.
Nothing about what I was saying precludes the use of design-by-contract; in fact, that's what I was advocating. You say that "at runtime [type assertions] are still valuable because the code will fail sooner rather than later," which is what I meant by identifying places where there is a possibility for the wrong type to be used, and putting your assertions there. Sticking type assertions on the inputs and outputs of every function you write would be unnecessary, inefficient and horrible to read:
def increment(n):
assert isinstance(n, int) or isinstance(n, float)
result = n + 1
assert isinstance(result, type(n))
return result
So as long as we can agree on that much, then we can agree that there is a value to being judicious about where you should and shouldn't use type assertions.
By the way I'm a fan of static typing as well, although I see value in dynamic languages too. And I definitely acknowledge the limitations of unit testing, although from a practical point of view, a comprehensive set of unit and functional tests is usually robust enough. And, of course, type systems don't make any guarantees against logic errors. :)
On your example, you're of course right. I'm also not a fan of checking the actual type in a dynamic language, since it defeats the purpose of it being dynamic. I like assertions that are more useful than that, like:
def sqrt(x):
assert x >= 0, "only defined for positive numbers"
last_guess = x / 2.0
while True:
guess = (last_guess + x / last_guess) / 2
if abs(guess - last_guess) < .000001:
return guess
last_guess = guess
Now clearly this helps, since it aids in readability (this function is defined for positive numbers only) and if you call it with a negative number, it will loop forever.
Well, it would be cooler if the compiler could check those contracts at compile-time and issue some helpful warnings, but at runtime they are still valuable because the code will fail sooner rather than later. Plus you can probably disable them completely, should you experience problems with performance in production.
They also serve as documentation for other developers, documentation that you're forced to keep in sync. This documentation is not about the actual business logic, that ends up being laid out in tests, but rather about interface specifications and invariants.
So there you have it - testing serves a different purpose.
[1] https://en.wikipedia.org/wiki/Design_by_contract