I have, roughly, sworn off dynamic languages at this point. Although I have dreams of implementing a firm typed system over Common Lisp.
https://coalton-lang.github.io/20211010-introducing-coalton/
?I change a function's arguments, I'm _pretty_ sure I caught every place it's called (spoiler alert: I didn't, because someone calls functions dynamically based on their names with call_user_func_array, maybe), and I ran the test suite - everything works, and I've fixed what doesn't.
Except some of that old code written 8 years ago didn't have good tests, or didn't have any tests at all, or didn't cover the specific code path.
And now it's 2am on the production server.
Now, you could tell me - Pavel, you're an engineer, you should be sufficiently dedicated to your craft to verify that the tests are testing those paths, you should find all those instances where that function is called (a few dozen times across the codebase), and check that those tests exist and are good. And you're not wrong! I should be doing cleanup in the codebase as I go, washing those dishes while I wait for pasta to boil or whatever...
... but now I've given myself two more actual engineer-weeks of work for a change that just needed to add a flag to a function to make sure only premium users get emails when something happens. My manager will be thrilled to hear it. Do you think I'll get a raise?
(I know Python type checks aren’t mandatory, but for this question assume that the type checker is running in CI)
You can definitely circumvent Go's type system with unsafe operations (with FFI also recognized as inherently unsafe) but that's a clear boundary that you know you're crossing (albeit not quite as clear as e.g. in Rust).
The fact that there still exist people to whom this is not painfully obvious is despairing me.
Even then, you're reliant on the correctness of a 3rd party tool vs a 1st party compiler. It's never going to be as good of an experience.
It's not about feeling. It's about the theorems you can make about the codebase. With Python the answer actually is ¯\_(ツ)_/¯.
Isn't that exactly what unit tests are for?
I understand that statically typed code doesn't mean bug-less code, but I always find it odd that dynamic language enthusiasts feel like they need to stretch the benefits. Dynamic languages are great for many things. Strictness is not one of them and that's fine.
Moreover, a good type system can also force the values to be right by enforcing certain invariants directly in the type system. For example, let's say I have a function `deleteProjectAs(user, project)` that deletes a project, but it only works if the user is an admin, otherwise it throws an error. I can write a bunch of tests that validate that this function checks that the user is an admin in all sorts of different cases, but with a type system I can write something like `deleteProjectAsUser(user: Admin, project: Project)`, and this guarantees at the type level that no non-Admin user can delete a project.
The point here is not that types can replace all tests, but that well-designed types can get rid of a lot of them.
Why are you doing double misrepresentation?
More honest discussion tactics, please.
I personally believe it's a valid argument (others will disagree). IMO the main benefit of static types isn't for correctness (nor performance) - it's to force programmers to write a minimal level of documentation, and to support IDE features such as autocomplete and red underlines. Hence the popularity of Python type hints and TypeScript, which provide these features but don't fully prove correctness nor provide any performance benefit.
In this regard, Rust (and other languages where lots of data invariants can be encoded in the type system) is very flexible and easy to maintain indeed, because you can easily make changes, even in very old or poorly-maintained code, without having to worry about the consequences. Moreover, rather than writing all the unit tests yourself, it's as if the compiler is writing the unit tests for you.
In fairness, you can't encode everything in the type system, so you still need unit tests in top of that, but in my experience you can get away with far fewer. In general, I would say that Rust's type system, when combined with unit tests, is far more flexible, powerful, and easy to maintain than dynamic Python with only unit tests.
1. Of course a type system is not as "flexible" as arbitrary test code.
2. Compiler-enforced type safety is many orders of magnitude easier to maintain than the equivalent unit tests
3. Defining rigorously enforced invariants with a type system is far, far more powerful than hoping you remembered to test all the important cases.
Of course if you limit yourself only to problems that can be effectively solved by type system, then it may work. It is like limiting yourself only to those text processing tasks where regexs work. Yes, some text processing tasks may be much more effectively solved using a regex. But it is obvious that at some point in a more general case grep can’t replace python. It may be less apparent for compiler vs. python case but the general logic is the same.
We all deploy to production, dude.
Strong static typing (Rust), even only dynamic strong typing + pattern matching (Elixir), are leagues better than Python. Literal 3x - 7x less code and less production errors both in Elixir and Rust, over the course of 5 years.
You're the one limiting yourself. Expand your horizons.
E.g. X is a list of strings Translate X to a list of indices Translate X back to a list of strings.
In that paragraph the input and output types are the same, but not complains about the second line.
I always have to introduce a variable with a new name.
Even in rust you have to be explicit about doing the same thing with an extra "let" statement.