15Kloc and trouble is indicative of other problems than a problem with the language. I wrote multiple 50Kloc and up pieces of software in GFA Basic, arguably a much more limiting and unsafe environment than Python ever was, and yet, that software worked well and was maintainable to the point that its descendants still run 3 decades later.
Python has all the bells and whistles you need to build large code bases, but you do need to get organized, no language can really do that for you and attempts at forcing you to do it give rise to 'factory factory' nonsense. I'm no huge fan of Python, I use it when I have to but Rust is from a bells-and-whistles point of view a step down from Python, it is more a better 'C' than a better Python and Go has it's own share of issues that has kept me away from it so far.
If you want to crank out some research project or application quickly building on the libraries that others provide (especially in machine learning or other data intensive fields) then Python would be my first choice, and something like 'R' probably my second (though I see 'R' mostly as a data scientist's workbench that has a programming language bolted on).
Most of my serious work has been done with statically typed languages, though of course I have worked with dynamically typed ones as well. There are a few things about dynamically typed languages that seem to productivity-negative, but it may just be my lack of understanding of how people expert at a dynamic languages work with them.
1 - When working with a large enough code base that you don't remember exactly what every function you have written needs, or using third party libraries, function signatures lacking type specifications make it harder to know what is expected. if there is documentation that is good, you can of course load that up, but this slows you down. IDEs today can usually show you the function signature as you type it, but with dynamic types you get less information.
2 - When refactoring something in a statically typed language, changing a data structure or type name will cause the compiler to error every single place that you need to adjust for your change. With a dynamic language you need to rely on unit tests or you will be finding places you missed during runtime for a long time afterwards. If you do rely on unit tests for this kind of thing, then you have sort of built an effectively statically typed environment, at some large degree of effort, with worse runtime performance to show for it.
I understand some of the workarounds people use to deal with these problem but a lot of them seem to push you to the point that you might as well use static typing and get better runtime performance for it.
Python has type annotations since 3.5 and you can get static type checking via mypy. This is a type system that works quite well IMHO and eleminates most of the issues you mentioned.
As someone who does most of their professional development in Python, I’ve been eagerly waiting for something like Mypy, but my experience with it has been really disappointing and frustrating compared to languages like Go. Mypy mostly seems immature, buggy, and completely unergonomic (typing support was shoehorned into the syntax so most things chafe—declaring a type variable or a callable that takes args and kwargs). Python needs a better static typing story in order to be a productive language in medium-to-large-sized projects.
> Mypy mostly seems immature, buggy ...
That was my experience some time ago as well, but it's getting better from release to release. Look at the repo [0], it's continuously improving. I find Mypy deserves more recognition.
The same guys working on mypyc [1], which I think is very interesting too.
[0]: https://github.com/python/mypy/graphs/contributors [1]: https://github.com/mypyc/mypyc