It can be hard to understand why developers would choose a dynamically-typed language over a statically-typed alternative. The benefits of static typing can be fairly obvious: catching mistakes using the wrong type, better editor support for features like auto complete and in-line documentation. The benefits of dynamically-typed languages can take some more thought to discover. So let’s look at some of those benefits. I’ve arranged them in order starting with benefits I think are the strongest but apply to the fewest developers, and ending with benefits that apply to the most developers but aren’t quite as strong.
Note: the Ruby language is working on a technology that blurs the lines between static and dynamic typing. TypeProf is a tool that executes your code, inspects the types actually used in the running program, and records type signatures in Ruby Signature files separate from your source. This allows IDEs to provide the usual benefits of type declarations (type checks, auto complete, and inline documentation) without developers having to write type declarations in their source files. This is an early-stage technology, and if it proves successful then a Ruby program using TypeProf would occupy a unique place in the pros and cons of static typing. You can learn more in my blog post about TypeProf and its IDE integration.
1. Research and Invention
If you’re writing your statically-typed code on a computer with overlapping windows with scrollbars, multiple fonts, and pop-up menus, you can thank dynamic typing for those features. All of them were invented within Smalltalk, a dynamically-typed language that was developed between 1973 and 1980. The same goes for unit tests, just-in-time (JIT) compilers, and agile programming methods: all invented within the context of Smalltalk.
The reason these innovations were able to be made in Smalltalk is that its programming environment is set up for research and experimentation, and a key part of that setup is dynamic typing. The source for all the code in the system is inspectable, and you can modify existing classes or write your own interchangeable versions. “Design Principles Behind Smalltalk” gives the example of “novices who just want to experiment with their own class of numbers.” Once your own class is compiled, it’s immediately usable within the running system and ready to be experimented with. The way this is accomplished is by dynamic typing. Smalltalk methods don’t check the types of arguments passed in. That way, if you create your own class that responds to the same messages, the rest of the Smalltalk system can use it without requiring any recompilation. (If you’d like to learn more about Smalltalk, take a look at my research on User-Modifiable Software.)
Now, most of us are not inventing new computing paradigms most of the time, so dynamic languages’ benefits for research don’t apply to us. But it’s a good reminder that not everyone writes software in the same way as us. Even if we never have a need for technology, that doesn’t mean no one does.
There’s an approach to object-oriented design that leans heavily on polymorphism. Instead of lots of conditionals in your code, you handle different scenarios by implementing different classes. This approach comes out of the Smalltalk world and is championed in Ruby by Sandi Metz in the book Practical Object Oriented Design. This approach can be taken in statically-typed object oriented languages where you define an interface, but the effort is lower in dynamically typed languages. In these languages, when defining a method you don’t have to decide whether to take an instance of a concrete class or whether to create a new interface, because there are no declared types. Every method is automatically compatible with any passed-in argument that responds to the messages sent to it.
I find this approach to object-oriented design very appealing—but, despite that, I haven’t found opportunities to use it on many if any of my projects. My side projects are very small in scope, and most of my professional work consists of React frontends, where the APIs and common patterns are based on functions instead of classes. Maybe I just haven’t spent enough time around people who use polymorphism extensively to learn from them. But if I—legitimately a Sandi Metz stan—haven’t managed to get in the habit of programming this way, I think it would be difficult to get an entire large team programming this way consistently.
If you are using polymorphism this way, then that’s a good argument for you to use dynamic languages. But I can’t strongly argue this point because it’s not the reason I choose them.
3. Refactoring and Evolutionary Design
This argument for dynamic typing centers around refactoring—what I’d like to be able to just call “refactoring” since it’s how Fowler defined the term, but since many in the industry use the word to refer to large changes I need to qualify as “small-step refactoring.” Dynamic typing lowers the effort for many refactorings, whereas static typing increases the effort for them. This effort comes from steps like having to create new interfaces, having to update type signatures throughout the app to use a different type, and simply the extra effort of writing out type declarations. Although IDEs can help automate some of these changes, it still takes more effort to use these automation steps than if the changes weren’t necessary in the first place.
Because of this extra effort in statically-typed languages, you’re more likely to only do a refactoring when you’re sure it will improve the code. With dynamic typing you’re more likely to do a refactoring as an experiment to see whether or not it helps. Either one is just as easy to revert if you’re using version control, but it feels more discouraging to throw away the larger amount of work that static types require.
This difference is extremely important if you’re following evolutionary design. In this approach, you write the code that is the simplest possible implementation of today’s requirements, and then tomorrow when you’re working on new requirements you refactor the system to be the simplest implementation of those. As you’re doing all of these many changes, the extra effort of static typing multiplies. You will be tempted to plan out in advance how your code should be structured (leading to unnecessary design complexity) or to leave code that is not ideal as-is (leading to code cruft). If you want to learn evolutionary design and stick with it rigorously, your best chance is in a dynamically-typed language.
Now, someone might say that they don’t do this kind of refactoring and so they don’t need dynamic types. My response is that the industry has a major problem with software maintenance, and legacy code is just as hard to work with as it always has been. As a result, I think we need more developers to learn how to do that kind of refactoring. And it’s easier to learn and more motivating to do with dynamic types than static types.
4. Not Having to Satisfy the Compiler
Type checkers and compilers can be the most frustrating when we can tell that our programs work, but the compiler can’t tell that until we give it more information. This is a case where the compiler isn’t working for your benefit; you’re having to work for the compiler’s benefit.
The degree to which problems like this come up likely varies between different statically typed languages, so there is a sliding scale on which the pain point lies.
5. Concise Code
Ruby developers in particular argue that code without type declarations is more concise, easier to read, and clear. Instead of lots of technical details, it’s easier to see the high-level intent of the code, the business logic.
Advocates of static typing might reply that it’s easier to read the code when you can see the types of the variables instead of having to guess them. Now, if you’re using a polymorphic approach in Ruby or Smalltalk, there is no type to show: the type is any object that can respond to the messages sent. But if you’re like me and there usually is just one type for the variable in your systems, then the argument goes that the type is clear from the context, so a type declaration is duplicative.
But how clear is it from the context, really? This starts to get into what people mean when they say that static typing is better for larger projects with larger teams. For a small project with a small team, any given developer is familiar with much of the code, so with that knowledge it’s more likely that the type will be clear from context for them. For a large project with a large team, any given developer is not familiar with most of the code in the project, so for them it’s more likely that the type will not be clear from context.
Advocates of dynamic typing say that unit tests provide documentation and safety. Do those help with this situation of identifying the type? With static types, editor assistance will tell you the correct type to pass into a method, and if you pass the wrong one you will get a type error. With dynamic types, you could read through the tests to look for examples of correct and incorrect types passed in, but that takes a lot more work, and you will be less likely to do it. And here’s a crucial difference: unit tests of a class don’t test its callers. They confirm that the class handles any inputs that the author of the class decides to test. But if the caller passes a type that is not covered by unit test, that’s unspecified behavior. There’s no way for a unit test to check the types that callers use. You can use defensive programming to check the inputs and fail early, but at that point you’re implementing runtime type checking in a verbose way, and it might be more readable to use a statically-typed language.
What is it about dynamic typing that’s so appealing to me? I’m interested in the power of dynamic typing for research and for polymorphism in theory, but in practice I haven’t done those things myself. What I have done is build systems with lots of attention to design: deeply understanding them, working hard to build just what I needed for the moment and to refactor them to fit new requirements. On projects like this, I’m constantly reading and changing the code, and dynamic typing means I can do that as quickly as possible. It’s a really enjoyable experience.
But it seems like the identical experience is not available for large teams. It’s not a matter of whether those teams are experienced with and committed to object-oriented design and test-driven development. It’s just not logistically possible for everyone to be deeply familiar with all of the code.
This is a good argument for keeping systems and development teams small when you can, so you can get the benefits of dynamic typing. Some Lisp and Smalltalk teams accomplished this by customizing their environment and creating specialized tooling, but this tended to make systems impenetrable to new developers. Basecamp tries to do this with Rails by designing architectures to serve even rich frontends and mobile apps out of a server-rendered backend, but in ways that I think have a lot of downsides for both development and user experience. Microservices try to break large systems up into much smaller ones, but this introduces the complexities of distributed systems.
So there is no silver-bullet way to keep any given system small, so that you can get the benefits of dynamic typing. Instead, what’s appealing to me is to focus on the kinds of systems that dynamic typing is the best fit for, so I can get the dynamic typing experience I enjoy: code exercises and experiments, personal projects, internal systems by small teams, and startups looking for product-market fit.