Recently I have worked to set up type checking in our CI pipeline using pyrefly. In this post, I want to share how to configure pyrefly and also fix typing hint related issues during the process.
which type checker to use in 2026?#
There are a lot of options for type checker: mypy, pyright, pyrefly, ty, zuban.
pyright and mypy is quite slow. Pyrefly, ty and zuban are implemented in rust and is quite fast all daily needs. Both prefly and ty are maintained/backed by companies and are likely to last long, while zuban is currently one-man project.
Note also zuban is licensed as A-GPL, which requires commercial license for companies.
Personally, i think just choose either pyrefly or ty if you are starting a new project.
In this type spec conformance, you can also check conformance of different tools to the Python typing standard: https://htmlpreview.github.io/?https://github.com/python/typing/blob/main/conformance/results/results.html
pyrefly config#
Not much config is needed for pyrefly to work, if you use pyproject.toml to store its config,
this is a sample config:
[tool.pyrefly]
project-includes = ["app"]
project-excludes = ["**/tests"]
search-path = ["app"]
output-format = "min-text"To suppress error, see https://pyrefly.org/en/docs/error-suppressions/
fix typing errors#
None type does not have certain attributes/methods#
# some_func() -> dict | None
my_val = some_func()
my_val.get("key1")For example, for this code, the type checker will complain about None type does not have attribute “get”. This can be fixed by narrowing the type.
my_val = some_func()
if my_val is None:
raise RuntimeError("my_val is not supposed to be None")
my_val.get("key1")or
my_val = some_func()
if my_val is not None:
my_val.get("key1")function that return incompatible types based on parameter#
def my_fun(transform: bool) -> dict | set:
if transform:
return set([1, 2, 3])
return {"data": [1, 2, 3]}
val = my_fun(tansfrom=True)
# the following will raise type error
data = val.get("data")For the line data = val.get("data"), type checker will report that
set does not have attribute get
This can be fixed by overloaded function.
from typing import overload, Literal
@overload
def my_fun(transform: Literal[True]) -> set: ...
@overload
def my_fun(transform: Literal[False]) -> dict: ...
def my_fun(transform: bool) -> dict | set:
if transform:
return set([1, 2, 3])
return {"data": [1, 2, 3]}Another way is to separate the function logic to different functions, which might be a better solution?
missing fields for TypedDict#
from typing import TypedDict, Required, NotRequired
class Config(TypedDict, total=False):
host: str
port: int
debug: Required[bool]
# Valid! Initializing empty works because all keys are optional:
settings: Config = {}
settings["host"] = "localhost"with total=False, you can initialize an empty dict for settings.
You can also combine with Required and NotRequired to meet your needs.
no matching overload#
Sometimes, pyrefly is inferring the types too eagerly:
params = {
"count": 0,
"name": "my-name",
}
params.update({"foo": [1, 2, 4]})For above code, pyrefly will infer params as dict[str, int | str], then it will complain about the
update statement since the value is a different type.
No matching overload found for function
typing.MutableMapping.updatecalled with arguments: (dict[str, list[int]]) [no-matching-overload]
This happens because pyrefly is eagerly inferring the value type for dict type.
Since it only sees int and string for dict value during the initialization, it concludes that dict value type is int | string.
When you try to use a dict value type of list, it will then error out.
In this case, I like more how ty is handling it.
The fix for pyrefly is simple, though.
You can type hint params as dict[str, Any], then pyrefly will not complain about this.
Overall experience#
I think pyrefly is a pretty fast and comprehensive type checker, which help you to catch potential bugs in your code. Sometimes, the error message can be hard to understand though.
Also be aware that pyrefly is still under active development and may have performance issues. I met one issue where pyrefly is extremely slow (took more than 1000 seconds to check a code snippet), but it was fixed quickly by the team.
reference:
- https://blog.edward-li.com/tech/comparing-pyrefly-vs-ty/
- https://pyrefly.org/blog/too-many-type-checkers/
- https://pyrefly.org/blog/speed-and-memory-comparison/
- https://python-type-checking.com/
- https://python-type-checking.com/typecheck_benchmark/
- https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare/