Dynamically typed languages and the illusion of velocity

Does evading explicitly providing types when we create software really make us more productive?

Dynamically typed languages and the illusion of velocity
Photo by Maico Amorim / Unsplash

So to start of — what exactly is a dynamically typed programming language? To put it shortly: it is a language where the type of a given variable is set and checked during runtime. This is in contrast with statically typed languages where the above is done usually during compile time (before the application is executed) by a type-checker.

In the last decade there has been a surge of new developers using dynamically typed languages for major software development projects where the norm was to use a statically typed one for that. This can be attributed mainly to the two behemoths that each dominate their respective fields, those are of course:

  • JavaScript (Brendan Eich, 1995)— The main purpose of the language during design and creation was to be embedded into a web browser to provide interactivity for web pages. Currently it has grown into a dominant force behind web app development on both the frontend (e.g. React, Node.js, Vue) and backend side (e.g. Next.js, Node.js) but also expanded into other non-web areas
  • Python (Guido van Rossum, 1991) — Created as a general purpose interpreted language Python has seen many different use cases along the years. This includes being a replacement for Perl in sysadmin and utility scripting, application scripting (e.g. Blender) , web development (e.g. Flask, Django) and currently it serves as the main tool for scientific computing, data science, engineering and machine learning

In regards to JavaScript there is an even more serious problem regarding the type system which is it being weakly typed. But we’ll leave that subject maybe to be discussed on another time as it would deviate the subject of the article.

Of course there is a plethora of other dynamically typed languages but the two above are the most well known and widely used so they will serve as a good platform to discuss the subject.

You don’t need to worry about types so you code faster!

The above statement is something you’ll find a lot when reading about the advantages of using something like Python instead of Scala or Rust — thanks to the lack of static typing you waste less time on “useless stuff” like types and can deliver faster something that works.

But I will argue that this is a myopic view and it quickly falls apart as we increase the complexity of an application (or a bigger system consisting of multiple ones) and the number of people which work on the codebase(s).

So maybe let’s start with a small example of implementing the same piece of logic in two languages — one dynamically typed, the other one statically. For that we’ll be using Python (dynamic typing) and Scala (static typing) — Scala being chosen as the language that contains a very powerful static type system that should contrast better with what we’ll get in Python.

So what we’ll be implementing is a very simple function that should:

  1. Take one integer as the argument
  2. Check if it’s equal to 42 and if so return the string “Wow!”, otherwise return an empty value

As you can see this is a extremely basic piece of logic. Here’s how it would look in Python:

def check_int(some_integer):    
    if some_integer == 42:        
    	return 'Wow!'    
    else:        
    	return None

As for Scala:

def checkInt(someInteger: Int): Option[String] =
	Option.when(someInteger == 42)("Wow!")

Though the above examples are really small even then we can identify a few issues that stem from Python’s approach to typing. These are:

  1. No information and guarantees for some_integer type— nothing will enforce that we actually pass an integer to check_int. It will gladly run when some random string is passed or a None, even though at that point we can easily tell that should not be allowed
  2. No information and guarantees for the return type — when calling this function we cannot tell what it will return and we cannot enforce the implementation to return a proper type. We can actually mix different return types in the body or forget a return statement which will cause it simply returning with a None
  3. We cannot effectively encode the information that the return type might be empty — in the Scala version this is explicit for the developer via the Option type and forces him to handle it. Although this argument can also be made for many statically typed language that base on null and exceptions to handle erroneous computation but then it is a matter of bad design, here it is impossible to do effectively
  4. General lack of information — the Scala function signature encodes much more useful information that make using and thinking about it easier. In a well typed application just looking at the types going in and out of a function gives us a lot. In the case of Python we simply know that it takes a single argument, most probably an integer judging from the name and return "something". And this argument expands further when we start playing around with classes and more complicated data structures, composed together on top of that

The first three combined result in a very major problem we’ll be facing (and the end users of the code) which is that we will encounter a lot of runtime errors which could have been easily avoided if we just enforced proper typing beforehand, many of them trivial but application-braking nonetheless. Consider the following:

left = 'some value'
right = True
i_take_hours_to_run()
print(left > right)

In the above of course we’ll get a type error and that is correct. The issue here is that we’ll get it after the application is ran — and if the naming of i_take_hours_to_run isn’t lying it will takes us a lot of time to get feedback on a very simple mistake that the type checker of Scala would easily catch before the execution.

In general we would want to minimize the amount of bugs and errors we get during runtime because of obvious reasons — it might affect the end user, it’s harder to debug and a few more nasty side-effects. There is also a fundamental software truth tied to that — as an application grows the number of branches the control can go into grows extremely fast and it is impossible to test all of them during runtime QA and unit testing. Reducing those untested scenarios/branches is something we should always aim for and static typing makes that a lot easier.

Static typing gives us a boundary and set of guarantees about our code which will result in less problems in the future as more bugs caught before the application goes into the world. Also a much better developer experience as useful information is encoded into the source code via the types. Write a simple app in a dynamically and statically typed language and then go back to it after a few months or even weeks — it is easier to pick up what is happening in the statically typed one if they were written with similar skill and approach.

Considering all of what was said above it is pretty easy to conclude that as the amount of code grows the potential issues grow with it — might be pretty simple to understand the whole code and manually guarantee no breakage in typing when working in a 1–2 people team on a small utility tool. But even for an medium sized CRUD-like application developed by a handful people (who probably change as time goes by) this very quickly becomes a real pain.

In short — static typing is an investment where the return grows as the application grows in size, complexity and number of people that work on it. The velocity given by dynamic typing quickly fades as time goes by.

Is dynamic typing always worse?

As with almost any problem in software engineering there is no perfect solution, just gray with different shades. Still, in my opinion for the majority of non-trivial use cases investing into a properly statically typed source code is the superior choice. But there are some scenarios where I would very much consider using a dynamically typed language and those are:

  • The language has superior 3rd party code and support for the given goal — Python in Data Science, ML and visualization is an example of that
  • A small MVP/PoC where it getting of the ground is most crucial
  • Game and application scripting — usually there is no need for any complex typing as we are working within the constraints set by the scripted application
  • Frequent, on-the-spot changes are required — the overhead of maintaining the static typing might very well be too much considering the gains
  • Small and medium sized utility/sysadmin tooling — in such scenarios we don’t really operate on complicated data types and logic

There are probably other ones I didn’t mentioned above but I think those capture the gist of it — small, low-complexity use cases with good 3rd party code and community support.

The tooling, libraries available and community support are very important things which are many times ignored when choosing a language, which I think is a big mistake. Having a well-written, maintained and developed library with great documentation and a hundreds of SO question/answers is a blessing that will make development using that language orders of magnitude easier.

Conclusions

Hopefully I haven’t stepped on too many toes and burnt too many bridges by what I wrote. I actually like Python and have done many stand-alone applications using it (tooling, Spark, Django, scraping and some others) but I thought it is important for people to be aware that languages like it have also some inherit drawbacks that might come up later, after the honeymoon phase has passed.

And looking at the growing adoption of TypeScript it seems many people are “seeing the light”, probably a lot of it has to do with the rising complexity of modern web apps and trying to apply JS for e.g. writing backend code. I am hopeful that this trend continues, in addition to the death of null and exceptions as standard ways of handling errors in computations.

I hope that I’ve sparked some curiosity into people who haven’t been before exposed too much to statically typed languages before and that they’ll give it a shot. It’s really worth it — even just for the sake of learning a new way of doing and thinking about building things with code!

Thank you for your time and let the types be with you!