by Ehud Tamir
How to quickly find type-issues in your Python code with Pytype
TL;DR — If you’re working on a large Python project or just like to keep your code-base tidy and neat, Pytype is the tool for you.
Python is a great programming language for prototyping and scripting. The concise syntax, flexible type system, and interpreted nature allows us to quickly try an idea, tweak it, and try again.
When Python projects grow, the flexibility that was once an enabler for speed becomes a burden on development velocity. As additional developers join the project, and more code is written, the lack of type information makes it harder to read and understand the code. Without a type-checking system, mistakes are easy to make and hard to catch.
Pytype to the rescue! Pytype is an open-source tool for type checking and type inference in Python. And it works out-of-the-box — just install and run!
- Statically infer type information and check your code for type errors.
- Validate PEP 484 type annotations in your code for consistency.
- Merge back inferred type information into your code, if you want.
If you’re sold, go ahead and visit Pytype for installation and usage instructions. Below I present some cool usage examples!
Example #1: Type inference and checking
This is the most common scenario. You wrote some code and want to sanity-check that you didn’t make any mistakes. Consider this function:
def GetUsername(email_address): match = re.match(r'([^@]+)@example\.com', email_address) return match.group(1)
Pretty straightforward. It extracts the part of an email address before the @ using a regular expression, and returns it. Did you notice the bug?
Let’s see what happens when we use
pytype to check it:
% pytype get_username.pyAnalyzing 1 sources with 0 dependenciesFile "/.../get_username.py", line 5, in GetUsername: No attribute 'group' on None [attribute-error] In Optional[Match[str]]
Pytype tells us that
group is not a valid function call on
None when no match is found. Indeed, in these cases
match.group(1) will throw an exception.
Let’s fix the bug, by having the function return None for an invalid email address:
import redef GetUsername(email_address): match = re.match(r'([^@]+)@example\.com', email_address) if match is None: return None return match.group(1) # <-- Here, match can't be None
Now, when we re-run
pytype, the error is gone. Pytype infers that if the code after the if gets executed, match is guaranteed not to be
Example #2: Validation of type annotations
In Python 3, you can type-annotate (PEP 484) your code to help type-checking tools and other developers understand your intention. Pytype is able to alert when your type annotations have mistakes:
import refrom typing import Match
def GetEmailMatch(email) -> Match: return re.match(r'([^@]+)@example\.com', email)
pytype to check this code snippet:
% pytype example.pyAnalyzing 1 sources with 0 dependenciesFile "/.../example.py", line 5, in GetEmailMatch: bad option in return type [bad-return-type] Expected: Match Actually returned: None
Pytype is telling us that
GetEmailMatch might return
None, but we annotated its return type as
Match. To fix this, we can use the
Optional type annotation from the typing module:
import refrom typing import Match, Optional
def GetEmailMatch(email) -> Optional[Match]: return re.match(r'([^@]+)@example\.com', email)
Optional means that the return value can be a
Match object or
Example #3: Merging back inferred type information
To help you adopt type annotations, Pytype can add them into the code for you. Let’s look at this code snippet:
import re def GetEmailMatch(email): return re.match(r'([^@]+)@example\.com', email)
def GetUsername(email_address): match = GetEmailMatch(email_address) if match is None: return None return match.group(1)
To add type annotations to this code, we first run
pytype on the file.
pytype saves the inferred type information into a
.pyi file. Then, we can run
merge-pyi to merge the type annotations back into the code:
% pytype email.py% merge-pyi -i email.py pytype_output/email.pyi
import refrom typing import Matchfrom typing import Optional
def GetEmailMatch(email) -> Optional[Match[str]]: return re.match(r'([^@]+)@example\.com', email)
def GetUsername(email_address) -> Optional[str]: match = GetEmailMatch(email_address) if match is None: return None return match.group(1)
The type annotations, including
import statements, are now in the source file.
For more usage examples and installation instructions, please visit Pytype on GitHub.
Thanks for reading!