I've always worked most heavily with dynamically-typed languages. They're a great choice for me and the teams that I've worked on because of the kinds of work that we're doing. It's pretty normal though for these teams to add team-members that are completely unaccustomed to working with dynamically-typed languages because they come from backgrounds where they worked mainly in statically-typed languages.
There are a few things about working with dynamically-typed languages that generally seem to blow the minds of new-comers that are accustomed to statically-typed languages, and I'll talk about a bunch of those things here. There's a huge amount of hype these days around type-systems too, and so I'd like to dispel any connotations that they're necessary in all cases, or that they're sufficient in any situation.
Just because you're disconnected from static types, doesn't mean there aren't static analysis tools. Every dynamic language I know has a linter that can be integrated with most common editors or IDEs, and the linters are quite good at telling you in real-time about a bunch of common typos and mistakes you can be making. Make them part of your CI build too, instead of the compile step you're used to in order to keep them out of your codebase.
I know a bunch of people in statically-typed languages already write a tonne of tests, but if you're one of those ones that thinks that the static type-checker negates the needs for tests, you're reeeeeeeeally going to have to get over that and start writing some tests if you're going to be successful with dynamically-typed languages (and it'd almost certainly improve your statically-typed codebases as well).
It's pretty normal to write more test code than application code in dynamically-typed systems. There are no quality efforts that I've ever seen that have a higher pay-off than an extensive test suite though.
I've sometimes heard the argument that static-typing can mitigate the need for a whole tonne of test-writing but I've never found this to be remotely true. If you're testing output types and not the actual output values, you're doing it wrong. Just test the values; it's much easier and much more valuable.
I'll be brutally honest with you: If you find yourself on your first project that uses a dynamically-typed language and it doesn't have an extensive test-suite, you're almost certainly in for a very rough time as the codebase or the team gets larger. I generally have the same opinion about statically-typed codebases as well though.
Of course you need runtime "type" validations. Just like in statically-typed codebases, you still need to verify user input, or that your data sources or 3rd-party services are giving you data in the format that you expect. You should absolutely do these validations that I'm going to call "edge validations" because they're on the boundary between your application and the outside world.
With that said, there's usually no good reason to sprinkle input-type validations on every internal function and method to try to replicate what you've had in the past with a statically-typed codebase. I agree that it's nice to assure quality internally (and I heavily use micro-tests for that), but 90% of the time a function will throw appropriately when it gets an argument type that it doesn't expect as soon as it tries to use it. Dynamically-typed languages employ Duck-typing, so that problems with input arguments are detected when and only when a function or method tries to interact with the argument in a way that's not expected. It happens at runtime, and the stack comes from somewhere deeper in function, but it's generally got all the information you need to debug it. Of course on its own debugging a problem like this would be much easier in a statically-typed language, but you've got a huge suite of tests not only to mitigate the problem of run-time-only detection but also to add validation of the actual values.
And with that said, I'll still sometimes (though very seldomly) add some assertions at the top of a function or method to ensure that the input arguments are valid. Often it's to check things about the input that most type-systems aren't much help with either, eg: "Is the number a positive one?" "Does the string start with 'http://'?". etc, etc.
I've always loved the way that statically-typed systems enforce a certain type of code documentation. The type annotations make it easier for me to understand how to consume the functions and methods in the codebase and also what to expect inside the function or method if I'm making internal changes. The great thing about the type annotations too is that they're gauranteed by the type-checker to be correct whereas regular old source-code comments seem to always quickly go out of date (not that you shouldn't still try to use comments appropriately).
I'm probably going to sound like a broken record here, but again the extensive test suite can be quite useful for this. A test shows you how to call those functions and methods and what they'll return. I often find the tests more valuable than types too when I have both because the tests will show me argument and return values and not just the types (ie, It's more valuable to know that add1(1)
returns 2
and not just a number type.)
Microtesting is specifically useful here too, because if you only do large black-box-style tests, your tests won't be very useful for internal documentation. It's also important to organize your tests in such a way that the relevant ones are very easy to find. For a microtest suite, you'll probably want to mirror your application file structure as much as possible. I'll certainly admit that statically-typed application code is better at putting that documentation right in your face as you're reading source code and that's really powerful, so at least try to make sure relevant tests are as easy to find as possible. (I haven't got any experience with Python's doctest, but it sure looks cool too.)
But most of all, when looking for valuable sources of code documentation, get used to considering tests and try to read them when you have questions.
Sometimes the most important thing that's required is an attitude adjustment. You're working with a different programming paradigm; you're going to have to do some things differently.
The fact of the matter is that static types are not necessary to...
These dynamic languages have accomplished a hell of a lot in computing. You shouldn't need much more evidence than that.