Levels of Python type annotations

Levels of Python type annotations

Python has optional type annotations - also called "type hints". Like this:

  1. def entry_to_dict(entry: Entry) -> dict:
  2. return {
  3. 'title': entry.title,
  4. 'num_likes': entry.num_likes,
  5. 'url': entry.url,
  6. }

The annotations here being "Entry" as the type for the "entry" argument, and "dict" as the return type.

In fact, there are at least 3 ways type annotations can be used:

  1. documentation
  2. to configure libraries and tools
  3. static type checking

These are so different, asking "do you use type annotations?" is really too vague. It's three separate questions.

The first is demonstrated by entry_to_dict() above. You are reading the code, and just by reading it, you know that entry should be an instance of a class called Entry, and entry_to_dict() should return a dictionary.

This by itself is really useful. And part of what makes it useful is how easy it is to include when you write the code. There's very little "friction" to dropping these types in as you bang out the method definition... And you can just skip them when they are too complex (or unknown) to specify.

(Bonus: if you're the type of developer who uses autocomplete in IDEs, type hints can improve its capabilities in some cases.)

The second case - configuring libraries and tools - is demonstrated in data classes:

  1. @dataclass
  2. class Entry:
  3. email: str
  4. when: str
  5. @classmethod
  6. def from_csv_row(cls, row):
  7. email = row['Customer email'].lower()
  8. when = parse_scheduleonce_date(row["Meeting date and time in Owner's time zone"])
  9. return cls(email, when)
  10. def __hash__(self):
  11. return hash( (self.email, self.when) )

See the "email: str" and "when: str"? @dataclass uses those annotations to do its magic.

A more complex example of using annotations for configuration: the FastAPI library. (Which, by the way, has a great article explaining how type hints/annotations work.)

The third use case, static type checking, is a different animal.

Because statically typed languages (like Java) have one advantage over Python: the type of every variable and expression is predefined, so the compiler can catch many errors at compile time that Python cannot...

In fact, Python itself cannot catch those until the line of code is actually executed...

And the promise of "static type checking" is to fix that. Using a tool called mypy.

To get the full benefits, though, requires an ongoing tax on your time.

You must essentially learn a new mini-language to specify types, with constructs like Optional[List[str]] and Tuple[_T, _T] that just look weird until you learn how they work. Then invest effort with every function, class, and method you write, to specify the annotations correctly, consistently, and thoroughly.

Which takes more effort than you would like, frankly. It becomes an ongoing tax on your codebase, paid in your attention while coding.

But the larger the Python application, the greater the payoff. And especially for larger, enterprise-grade software systems, the benefits start to greatly outweigh the costs.