4 Python type checkers to keep code clean
- 06 October, 2020 13:51
In the beginning, Python had no type decorations. That fit with the overall goal of making the language fast and easy to work with, with flexible object types that accommodate the twists and turns of writing code and help developers keep their code concise.
Over the last few years, though, Python has added support for type annotations, inspiring a whole culture of software devoted to type checking Python during development. Python doesn’t check types at runtime — at least, not yet.
But by taking advantage of a good type checker, riding shotgun with you in your IDE of choice, you can use Python’s type annotations to screen out many common mistakes before they hit production.
In this article we’ll delve into four of the major type checking add-ons for Python. All follow roughly the same pattern, scanning Python code with type annotations and providing feedback. But each one offers its own useful additions to the basic concept.
Mypy was arguably the first static type checking system for Python, as work on it began in 2012, and it’s still under active development. It is essentially the prototype for how third-party type checking libraries work in Python, even if many others have come along since and expanded on its features.
Mypy can run standalone, or from the command line, or it can work as part of an editor or IDE’s linter integration. Many editors and IDEs integrate Mypy; Visual Studio Code’s Python extension can work with it directly. When run, Mypy generates reports about your code’s consistency based on the type information it provides.
If your code doesn’t include type annotations, Mypy will not perform the vast majority of its code checks. However, you can use Mypy to flag unannotated code. This can be done with varying degrees of strictness depending on one’s needs.
If you’re starting from scratch with a codebase and you want a preemptively aggressive linting strategy, you can use the
--strict option to prevent any untyped code.
On the other hand, if you’re working with a legacy codebase that doesn’t have many type definitions, you can use more relaxed options such as preventing only untyped function definitions with
--disallow-untyped-defs while allowing other untyped code. And you can always use inline comments like
# type: ignore to keep individual lines from being flagged.
Mypy can make use of PEP 484 stub files when you want to use type hints for a module’s public interfaces. On top of this, Mypy offers
stubgen, a tool that automatically generates stub files from existing code. For untyped code the stub files use generic types, which you can then mark up as needed.
Pytype, created by Google, differs from the likes of Mypy in using inference instead of just type descriptors. In other words, Pytype attempts to determine types by analysing code flow, rather than relying strictly on type annotations.
Pytype errs on the side of leniency whenever it makes sense to do so. If you have an operation that works at runtime and doesn’t contradict any annotations, Pytype won’t squawk about it. However, this means that some problems that should be flagged (e.g., declaring a variable with a type at one point and then redefining it in the same context) pass by unannounced. The documentation states such things will be disallowed at some point in the future.
If you choose to add type annotations to your code, then Pytype’s
reveal_type function comes in especially handy. If you insert a statement in your code that reads
reveal_type(expr), Pytype evaluates
expr and emits an alert that describes its type.
Note that certain Pytype behaviours are controlled by adding attributes to the code itself. For instance, if you want to stop Pytype from complaining about missing attributes or module members that are set dynamically, you have to add the attribute
_HAS_DYNAMIC_ATTRIBUTES = True to the class or module in question, as opposed to setting some kind of Pytype configuration metadata.
Pyright / Pylance
Pyright is Microsoft’s Python type checker, included as part of the Pylance extension for Visual Studio Code. If you’re already a VS Code user, the Pylance extension is the most convenient way to work with Pyright; just install it and go. Pyright provides a good all-in-one type checking and code linting experience, with many of the same conveniences and advances as previous Python analysis tools.
Like Pytype, Pyright can work with codebases that don’t have any type information. In those cases, Pyright will do its best to infer what types are in play. Thus you can still get good results with Pytype on older codebases with no type declarations. But you’ll get better results over time as you progressively add type annotations to your code.
Pyright is highly flexible in ways that complement the designs of real-world Python projects. As with other type checkers, Pyright can be configured on a per-project basis with a JSON-formatted configuration file in the project’s directory. Individual paths can be excluded (never checked) or ignored (errors and warnings suppressed) in the config file, and the options are highly granular.
In VS Code, workspaces with multiple roots can each have their own Pyright config, in case different parts of the project need different linting configurations. In the same vein, you can define multiple “execution environments” within a project, each with its own venv or import paths.
Created by developers at Facebook and Instagram, Pyre is actually two tools in one: a type checker (Pyre) and a static code analysis tool (Pysa). The two are designed to work hand-in-hand to provide a higher level of checking and analysis than other tools, although the user needs to do a little heavy lifting to take full advantage of them.
Pyre takes an approach similar to Pytype and Mypy. Untyped code is handled more leniently than typed code, so you can begin with an untyped Python codebase and add annotations function by function and module by module.
Toggle on “strict mode” in a module, and Pyre will flag any missing annotations. Or you could make strict mode the default and opt out at the module level. Pyre will also work with .pyi-format stub files.
Pyre has a powerful feature for migrating codebases to a typed format. The
infer command-line option ingests a file or directory, makes educated guesses about the types used, and applies the annotations to the files. You’ll want to make backups of your code first, though! (If you want to obtain type information from a running Python program, you can do that with another Facebook/Instagram project, MonkeyType.)
While Pyre’s features echo those of the other packages detailed here, Pysa is unique. Pysa performs “taint analysis” on code to identify potential security issues, relying on a library of flow analyses for certain software components and flagging code that appears to be vulnerable.
Anything touched by that code will also be flagged as tainted, though you can specify components that sanitise data and remove that data from the taint graph.
One drawback is that Pysa’s library of third-party component taint analyses is still small, so you might need to devise your own model. But many of the taint analyses are for software that is widely used, such as the Django web framework, the SQL Alchemy ORM, and the Pandas data science library, not to mention analyses for common filesystem issues.