Production-Ready Python: Type Checkers as Code Quality Gates
Python is a garbage collected language, You typically don’t worry about memory management through the lifetime of a program.
Python is a dynamic language. You don’t worry about curly braces, defining types, the borrow checker. etc.
Python is an explicit language. This means reading python is intuitive, writing python also can be intuitive.
But when it matters most, during runtime, the dynamic type of python can be a source of pain.
def process_user_data(data):
return data.upper() # What if data isn’t a string?
# Later in your code…
result = process_user_data(4) # AttributeError: 4 has no attribute ‘upper’
I got introduced into the world of type checks with Mypy (sound familiar?), but there are other type checkers written in Rust. It’s not all gloom, this subset of problems python introduces by being dynamic do have solutions.
Using the inbuilt typing module hints or Pydantic in python, can be used to annotate variables with types. As much as we’d love it in python, this does not transfer the benefits of static dispatch onto python.
Type hints can only do so much, in application to typed functions. When it comes to the underlying logic of the code, it’s advisable to use static type checkers.
The benefit of type-checking tools is they bring compile-time check to Python. They spot bugs that may occur due to syntax, data structures, types before running a program.
For example: One of the multiple checks mypy performs on a codebase is checking that accessed attributes on variables are defined. This is not to say programs which pass type checks will not have bugs, but these bugs will not be due to syntax.
How to integrate
Adding as an IDE extension is a good way to get immediate feedback as you write code, while adding as a step in CI is a good way to maintain code quality in a codebase of multiple developers.
Getting started with a static type checker is pretty easy, as easy as installing a package with pip/poetry/conda/uv/venv.
In this example, we will use both mypy and pyrefly as static type checkers.
Installing and Basic Usage
To install :
pip install mypy / pip install pyrefly
To analyse, I run :
mypy . — namespace-packages — follow-untyped-imports
This resolves a common error in mypy where library stubs not installed, an example:
Library stubs not installed for “pandas” [import-untyped]
Configuration
There are numerous ways of customising your Mypy configuration, with these configurations you can progress along the slider of strict -> lax configuration as you wish. Chances are you develop a custom configuration different from mine which suit your organization’s coding standards. You can neatly insert this into your workflow by adding a [tools.mypy] in pyproject.toml, creating a mypy.ini file or modifying your extension settings in IDE.
Pyproject.toml example
[tool.mypy]
python_version = “3.11”
namespace_packages = true
follow_imports = “silent”
ignore_missing_imports = true
A quick way to utilize all of mypy type safeguards is to add the –strict flag as in
mypy . — namespace-packages — follow-untyped-imports — strict.
This logically leads to more errors to resolve, but this can harm developer productivity in the stage of prototyping a solution, however it’s worth being aware of, for selective use.
After your Mypy configuration is set, you can save time on multiple runs with `-–incremental`. This enables mypy caching, specifying a cache directory with — cache-dir or a database with — sqlite-cache.
mypy . — incremental — cache-dir=.mypy_cache
Adding this configuration ensures the time taken to run Mypy for static typecheck doesn’t increase linearly with the size of the codebase on each check. However, caches can be invalidated with refactors, and a faster type checker may just be the solution to developer productivity. This is where we mention Pyrefly from Meta. This typechecker is written in Rust, which natively translates to faster checks, even better Pyrefly enables parallelised checks which means even faster computation.
Pyrefly: Speed and Intelligence
Pyrefly check
While design philosophy of both Mypy and Pyrefly is not exactly the same, for example defaults on handling untyped functions, Pyrefly is configurable to mimic either your existing Mypy configuration or pyright solution when you initialize pyrefly, with pyrefly init from the directory which contains your pyproject.toml / mypy.ini / pywright.json. Pyrefly configurations are stored in pyrefly.toml or pyproject.toml with [tool.pyrefly] section.
[tool.pyrefly]
search_path = [“src”, “tests”]
ignore_errors = false
respect_gitignore = true
A feature worth mentioning with Pyrefly is automatic type annotation. An unannotated codebase can be annotated with associated types
pyrefly infer — untyped-def-behavior check-and-infer-return-type –imports true — threads 6
This instructs Pyrefly to annotate existing code and infer types from first usage, and parallelise operations across 6 threads. This feature empowers you as a developer, who wants to add type-checking to your codebase to do so automagically. Integrating nicely with other validation frameworks/type checking frameworks with like Pydantic.
Before :
def calculate_total(items):
After :
def calculate_total(items: list[dict[str, float]]) -> float:
Impact on Production
Typical workflow (Type checkers → Tests → Staging → Production)
Type checkers serve as the first line of defense in this pipeline, catching syntax errors, type mismatches, attribute access issues, etc before code even reaches unit tests. This approach means fewer surprises in later stages and faster feedback loops for developers.
In practice, this means better code reviews focused on logic rather than catching type-related bugs.
Conclusion
Mypy, despite being slower compared to python, feels more native to python (This is entirely subjective). Pyrefly’s presentation of errors in terminal makes it easier to spot points of failure in code.
Downsides of using type checking tools
- There may be false positives. Assessing and Ignoring these can be time-consuming, (grows with the size of codebase/ added code). A clean way to implement this will be from the start of the project such that merged code are already reviewed and a pile of work is avoided.
- Compatibility with frameworks, while checking can be a struggle. For example: Pyrefly is unable to access certain associated attributes in the pandas, sqlalchemy API.
Helpful Links
- VSCode Extension: https://marketplace.visualstudio.com/items?itemName=meta.pyrefly
- Mypy Extension: https://mypy.readthedocs.io/en/stable/extending_mypy.html
- Pyrefly Docs: https://pyrefly.org/en/docs/
- Mypy Docs: https://mypy.readthedocs.io/en/stable/