TS
RSS

Wednesday, 28 April 2021

Does Python need types?

Python is famously a dynamic language, and many attribute its success to its dynamically typed nature. But is that really all there is to it?

Does Python need types?

I'm a huge fan of Python. It's by far the simplest general purpose language, that you can just pick up and start building amazing things with.

But for the past year or so, I've been working on frontend projects, and I've really enjoyed using Typescript. It's essentially JavaScript, but with fancy features built on top of it like Static Type checking and Null safety, and it was awesome how much it helped in writing robust, bug free code.

So I went out to find if Python has such an equivalent, and sure enough, there was.

It's called mypy, and it is amazing. It works so well in-fact, that I can never go back to writing plain Python now — and this article will be your introduction to it.

But before all that, let's figure out what's the deal with Types.

Why types?

What it essentially means is if you have a type system, every variable has a pre-decided type associated with it.

image

Untyped vs. typed Python code

Note that, while this might not look like it, the typed version is perfectly valid Python code.


What it also means, is you can't pass values of the wrong type anywhere. The type checker doesn't let you.

image

Which is extremely valuable if you think about it - I've lost count how many TypeError's I've seen in Python over the years!

Just having the confidence that there's no such place in your code where accidentally passed a str where an int was expected, eliminates an entire class of bugs from your codebase.


Not only that - you get a bunch of other benefits, namely:

  • Self-documenting code
  • None-awareness
  • Better autocompletion and IDE support

I'll go over all these points in detail.

Self-documenting code

Imagine you have this piece of code:

def add_orders(self, orders):
    for order in orders:
        self.pending_ids.add(order.id)

Seems rather simple, doesn't it? We seem to have a list of order's, and we add each order's id to a set called pending_ids.

But what are order's here...

It's hard to tell. In a large codebase, you might have to search pretty hard to find out which part of the code is calling add_orders, and where the data in that is coming from, to eventually find out that it's supposed to be just a namedtuple.

How about this instead:

from models import Order

def add_orders(self, orders: list[Order]) -> None:
    for order in orders:
        self.pending_ids.add(order.id)

Now it's instantly clear, that everywhere add_orders is used, it's going to be exactly of that type.

None-awareness

What I mean by this, is that not only can you not pass wrong types of values around, you also can't pass values that could be None, to places that don't expect the value to be possibly None.

Here's an example:

User = namedtuple('User', ['name', 'favorites'])

def fetch_users():
    users = []
    for _ in range(3):
        user_dict = get_user_from_api()
        user = User(
            name=user_dict.get('name', 'Anonymous'),
            favorites=user_dict.get('favorites')
        )
        users.append(user)

    return users

def print_favorite_colors(users):
    for user in users:
        print(user.favorites.get('color'))

users = fetch_users()
print_favorite_colors(users)

... and on first glance, this looks fine. We're using .get so we shouldn't get a KeyError anywhere, so we should be fine, right?

Now here's the typed version of the same code:

class User(NamedTuple):
    name: str
    favorites: Optional[dict[str, str]]

def fetch_users() -> list[User]:
    users = []
    for _ in range(3):
        user_dict = get_user_from_api()
        user = User(
            name=user_dict.get('name', 'Anonymous'),
            favorites=user_dict.get('favorites')
        )
        users.append(user)

    return users


def print_favorite_colors(users: list[User]) -> None:
    for user in users:
        print(user.favorites.get('color'))


users = fetch_users()
print_favorite_colors(users)

And as soon as you add types, you see one error:

image

You forgot that user.favorites could be None, which would crash your entire application.

Good thing mypy caught it before your clients did.

Better autocompletion and IDE support

This is honestly my favorite part of working with typed Python. The amount of autocompletion static types give me is awesome, and it increases my productivity ten-fold, because I rarely have to open the documentation anymore.

image

Where can I use it?

Now I can hear you saying, "All of this sounds very cool. But where can I use this mypy-thing in my Python codebase?"

And turns out, you can start gradually adding types to your existing Python codebase, one function and one class at a time. It will infer as much information as it can from the amount of type information it has, and will reduce your bugs no matter how small you start.


Conclusion

So, this was my introduction to you, to the world of static type checking in Python. Are you interested in learning more about it? I'll be dropping a detailed guide to mypy very soon, so stay tuned.

UPDATE: It's out!

I'd also love to hear your thoughs on this article, so let me know what you think about mypy down in the comments.