As a long-time Python and JavaScript user, I've come to the conclusion that dynamic typing is just not a good idea for anything beyond exploratory or very small projects.
The problem is that you invariably have to think about types. If you mistakenly pass a string to a function expecting an integer, you better hope that that is properly handled, otherwise you risk having type errors at runtime, or worse—no errors, and silent data corruption. That function also needs to be very explicit about this, but often the only way to do that is via documentation, which is often not good enough, or flat out wrong. All of this amounts to a lot of risk and operational burden.
Python's answer has historically been duck typing, which doesn't guarantee correctness so it's not a solution, and is more recently addressing it with gradual typing, which has its own issues and limitations. Primarily that if specifying types is optional, most programmers will not bother, or will just default to `any` to silence the type checker. While for JS we had to invent entirely new languages that compile to it, and we've reached the point where nobody sane would be caught working with plain JS in 2024.
Static typing, in turn, gives you a compile time safety net. It avoids a whole host of runtime issues, reduces the amount of exhaustive and mechanical tests you need to write, while also serving as explicit documentation. Code is easier to reason about and maintain, especially on large projects. You do lose some of the expressiveness and succinctness of dynamic typing, but what you gain with static typing is far more helpful than these minor benefits.
Dynamic typing with deeply nested data forces you to put type bandaids all over the code. For example you end up defining Pydantic schemas and then validating the same thing more than once since you can't guarantee that the type of a thing was not changed somewhere in the middle.
Dynamic typing forces you to test behavior which could be tested much more thoroughly by a type checker, at compile time, with zero development time.
Dynamic typing does offer much faster time to early prototyping but then drags you down with each bug.
Static typing does force some early commitments to the structure of the data but it also allows faster iteration and refactoring.
Static typing with good type inference seems the best to me.
I was strongly in the static typing camp for a long time, with Haskell, Purescript, Idris, OCaml, and Typescript, but over time I realized that the costs mostly outweigh the benefits.
> The problem is that you invariably have to think about types. If you mistakenly pass a string to a function expecting an integer, you better hope that that is properly handled, otherwise you risk having type errors at runtime, or worse—no errors, and silent data corruption.
The silent data corruption is really only a problem with weak dynamic typing, that usually automatically coerces types. A lot of dynamically typed languages still have strong typing and will immediately error out. And usually in practice you end up testing all of the code you're writing anyone, so this almost never happens in practice except when someone is refactoring without thoroughly testing what they did, which should be done anyway whether there are types or not.
> Primarily that if specifying types is optional, most programmers will not bother, or will just default to `any` to silence the type checker.
They probably do bother when it's an important module, or a an edge boundary that needs to be documented with a contract, or during times of significant refactoring. And these days LLMs can generate the specs, optional types, and tests pretty easily for any sort of self-contained, modular, reasonably well written code.
So now with LLMs I think there's even better reasons to use dynamic typing. And type completion in an IDE still exists for a bunch of dynamically typed languages anyway, like javascript.
> The silent data corruption is really only a problem with weak dynamic typing, that usually automatically coerces types.
That's not necessarily true. A function could serialize the passed value, which would work without type conversion, and it could still result in data corruption somewhere down the line. The point is that with dynamic typing there's no guarantee of correctness. It has nothing to do with strong vs. weak typing, which incidentally I don't find helpful debating, since there's no single definition for those terms, and most languages can behave arbitrarily depending on the situation.
Furthermore, you ignored my primary point of runtime type errors. These are very common in Python, and there's really no solution to them besides doing offline type checking, which as I said, has its own problems and is not a silver bullet either.
> And usually in practice you end up testing all of the code you're writing anyone, so this almost never happens in practice except when someone is refactoring without thoroughly testing what they did, which should be done anyway whether there are types or not.
Assuming you were referring to data corruption, maybe. But type errors happen very often in practice, and no amount of testing can guarantee you won't run into them. Besides, most teams I've worked with weren't disciplined enough to achieve even 100% statement coverage, let alone branch coverage, or do more sophisticated testing like fuzzing. So while type errors are close to impossible to prevent by testing, even data corruption can easily fly under the radar.
Static typing gives you this safety net, _for free_. This alone is worth the minor inconvenience of having to specify type information, and think about types explicitly.
> They probably do bother when it's an important module, or a an edge boundary that needs to be documented with a contract, or during times of significant refactoring.
This requires experience to know good practices, when to follow them, and the discipline to do so. IME very few developers are this diligent 100% of the time, and most, if given the option, will do the minimum amount of work necessary. I'm not just blaming others, I've been lazy about good practices myself many times. This is why gradual or optional typing is not a solution to these issues.
Looking at it from the other direction, most statically typed languages can do type inference. This avoids the tedium of having to be explicit all the time, while still giving you the benefits of type checking at compile time. This is a much safer solution.
> And these days LLMs can generate the specs, optional types, and tests pretty easily for any sort of self-contained, modular, reasonably well written code.
Seriously? LLMs have no place in a discussion about correctness. They're glorified autocomplete engines, which can be useful, but trusting them to give you correct output for these issues is incredibly risky. At best you would need to manually verify everything they do, and I trust myself to do a quicker job in most situations with macros and `sed`.
> And type completion in an IDE still exists for a bunch of dynamically typed languages anyway, like javascript.
I feel like we're talking about two different things, and you're ignoring the main issue of type errors at runtime.
The problem is that you invariably have to think about types. If you mistakenly pass a string to a function expecting an integer, you better hope that that is properly handled, otherwise you risk having type errors at runtime, or worse—no errors, and silent data corruption. That function also needs to be very explicit about this, but often the only way to do that is via documentation, which is often not good enough, or flat out wrong. All of this amounts to a lot of risk and operational burden.
Python's answer has historically been duck typing, which doesn't guarantee correctness so it's not a solution, and is more recently addressing it with gradual typing, which has its own issues and limitations. Primarily that if specifying types is optional, most programmers will not bother, or will just default to `any` to silence the type checker. While for JS we had to invent entirely new languages that compile to it, and we've reached the point where nobody sane would be caught working with plain JS in 2024.
Static typing, in turn, gives you a compile time safety net. It avoids a whole host of runtime issues, reduces the amount of exhaustive and mechanical tests you need to write, while also serving as explicit documentation. Code is easier to reason about and maintain, especially on large projects. You do lose some of the expressiveness and succinctness of dynamic typing, but what you gain with static typing is far more helpful than these minor benefits.