Skip to content

Static Typing

Python is a dynamically typed language, and this property has led many to claim that Python cannot be used as a primary language of large scale software systems. It is true that maintaining large systems with dynamically typed code quickly becomes a very large burden, and can grind development to a halt.

Perhaps in response to Python's growing popularity among developers of such large-scale software systems, in recent years, many enhancements have been made to the Python specification that allow for meaningful static typing. Since the inclusion of type hints by PEP 484 in 2014, a growing proportion of new PEPs have been focused on this area of the language.

It has been stated by the leaders of the language that Python will never become natively statically typed. However, type checking programs have emerged that run separately from the Python executable in order to perform the static type checks that the interpreter itself lacks.

It is our opinion at Paystone that for Python to be successfully employed as a primary language of any production software, the use of a type checker is absolutely required. In this article, we'll establish the benefits of a static typing, the expectations it places on machine learning engineers, and the reasoning behind our choice of type checker.

Benefits of Static Typing

To borrow a concept from Prefect, there are two types of work that an engineer performs when writing code: positive engineering, and negative engineering. To briefly summarize the concept, positive engineering means writing code that achieves an objective; negative engineering means writing code to make sure the positive code runs successfully and failure modes are handled correctly. An engineer's productivity is proportional to the amount of time they spend on positive engineering.

The reason for incorporating static typing into a codebase is to increase the amount of time an engineer spends on positive engineering tasks. How does it accomplish this?

  • When there are no constraints on the data types with which a function can be invoked, there is an entire class of failure modes that require handling. Static type checks prevent these failure modes from being reached.
  • Code becomes substantially more readable when it is clear what the types of its inputs and outputs are. More readable code means less time spent comprehending existing code, which means more time spent applying existing components to new functionality.
  • Statically typed code naturally produces efficient factorizations. Refactoring is a negative engineering exercise that is at times necessary, but is quite far removed from impactful customer benefits. It happens most often when a system's components have ill-defined responsibilities. Statically typed function signatures create clarity of purpose and establish responsibility. This drives effectively designed systems, which reduces the need for re-factoring, and increases time spent on positive engineering.

It is true that for an engineer unfamiliar with statically typed code, there is a non-trivial learning curve. For such an engineer, they may feel as though the type checker gets in the way and reduces their velocity of development. As one becomes more familiar with these rules, however, they begin to form a mental model for software systems whose basis is the type system. A capable engineer intimiately familiar with the type system uses it to map out implementations at a level of abstraction that enables high-velocity development. At a certain point, it crosses over from being a hinderance to one of the most valuable skills a software engineer has.

Expected Usage

A type checker should be running at all times when writing code that is meant for a Paystone repository. This does mean there is an exception when coding in Jupyter notebooks, but this is the only exception.

Types should be as specific as possible. While it is possible to "get around" a type checker by relaxing the type constraint of a given variable or function signature, this practice is greatly discouraged and should be prevented through peer review.

Practically, this means that no arguments to a function nor return values should go untyped, even if that type is the None type. It also means that the usage of the "Any" type should be restricted, although it is not prevented. The Any type is a valid annotation in certain situations, but it should be avoided as a means of subverting the type checker. There is almost always a better answer when it comes to typing a signature, even if it requires some re-factoring.

It is perfectly acceptable to spend a chunk of time working on what is primarily a typing issue. Optimal types are as important as optimal implementations, and one should not feel like they are wasting their time by focusing on this aspect of their code for an extended period of time. This investment will pay off in future iterations on the given area of the codebase. Of course, this is also a good time to ask for help from fellow engineers, as often typing challenges are common to many areas of the codebase, and others may have encountered similar problems.

When a third-party library has an untyped interface, it may be a worthwhile investment to create type stubs for the library. Type stubs are files which contain only function signatures, without implementations, where the signatures are fully typed. These files are used by type checkers when analyzing invocations of functions from the library. Determining whether this work is worth the effort is something of a subjective exercise. The goal is to balance the time required to create the stubs against the frequency of their use. If a third-party library is used commonly throughout the codebase, it is likely worth creating stubs for. If it is used in relative isolation in one component, it is unlikely to be worth the effort. Partial stubs are also acceptable; we need not stub the entire interface of the library, only the parts which we intend to use.

Why Pyright

During the initial phase of integrating static typing into the machine learning codebase, we opted to use MyPy. In the spring of 2022, we made the switch to Pyright. Other type checkers have not been considered so far.

Pyright is a Microsoft-backed project whose development is opinionated, clear, and efficient. Frankly, it is among the more impressive open-source projects in the Python ecosystem: its open issue count consistently remains under 10, despite having a small team of developers. This is in comparison to some ~2000 issues consistently open on Mypy.

Mypy focuses on extensibility and strives to accomodate everyone. While this is a desirable quality in some ways, a type checker that is opinionated in the rule set it chooses to enforce -- provided the rule set is good -- is a more efficient tool. Configuration of Mypy can be complex; its interface presents as something continually being patched, rather than the result of a directed effort. Because of this, Pyright has consumed considerably less engineering time in trying to integrate it into our processes.

While the community of Mypy is larger, the maintainers of Pyright are astoundingly quick to address bugs, release new features, and respond to inquiries by users. On multiple occasions our own engineers have reached out via the Github issue board, and received a response directly from the core maintainer of Pyright within an hour. This is quite a selling point for the tool.

The efficiency of Pyright is also notable. It can easily be run on an entire codebase with a single execution, and its design focus is on efficiency in large-scale systems. In contrast, due to its fragility when it comes to navigating codebases, Mypy must be run multiple times over particular subsets of a large codebase in order for it to collate all relevant code.

Anecdotally, there have been cases during our experience with the transition between tools where Pyright has identified typing errors in code which Mypy found to be acceptable. This is worth noting.

Overall, Pyright is an efficient, easy-to-use, opinionated type checker with a smaller customization surface, but with a stronger sense of direction and a very helpful and dedicated development team.