Type checking
Python is originally a dynamic and implicitely typed language. However, dynamic typing can be the source of many bugs, so in an attempt to make Python code more robust, static type checking with type hints was introduced.
It is important to understand that type hints have absolutely no effect on the execution of the code. Good type hints, no type hints, wrong type hints will always produce the same result at runtime. Type hints are only useful when combined to the use of a static type checker.
The static type checker will analyse the variable and functions to make sure their usage correspond to the type they were declared with.
As an example, with proper type hints, None
types should always be handled so NoneType
related errors should be eliminated.
This is similar to using Optional
in Java to avoid NullPointerException
.
Caveats
Some notable peculiarities of static type checking:
A variable cannot change type once it has been declared.
Containers cannot contain objects of types that are unknown at the time of the container’s declaration. By extension, this also greatly limits the usage of
*args
and**kwargs
which should now be avoided, as it is not possible to pass or forward arbitrary arguments anymore.Duck typing should be forgotten and proper polymorphism used instead.
Conforming to strict type checking can sometimes be difficult, but this may reveal that the code or architecture is too complex to begin with, so it can be a good opportunity to simplify and write the code in a more straightforward manner.
As the name implies, static means that behaviours that are too “dynamic” will not be compatible with static type checking.
Type checking in Qualia
In Qualia, “strict” type checking is used as much as possible.
There’s no universal definition of “strict” type checking as it depends on the type checker and its rule set.
However, it generally means that you cannot have partially type-hinted code or uknown types.
Type parameter for generics, e.g., containers like list[]
are important.
At the time of writing, not all modules have been fully typed. Some external libraries are not typed either.
In Qualia, the default type checker is mypy as it is slightly more flexible than other type checkers and is smarter at infering some complicated types. However, it has extremely poor performances and it is completely unreasonable to use during development as a result.
For development, it is highly recommended to use pyright.
In the future, a complete switch to pyright might be considered.
Postponed evaluation of annotations
Generally, modules should have from __future__ import annotations
at the top of the file in order
to enable postponed evaluation of annotations.
Among other perks, this allows using some newer annotations syntax like |
on older versions of Python, provided the type checker understands them.
This also allows forward references.
Union type with |
Postponed evaluation of annotations enables the use of newer features that may not be compatible with Python 3.9 (currently the oldest supported version for Qualia).
Therefore, it is recommended to write Union annotations as X | Y
and Optional annotations as X | None
as recommended by PEP 604
Custom TYPE_CHECKING constant
With PEP 563, modules that are only used for type checking should be enclosed
in a if typing.TYPE_CHECKING
block.
However, this currently prevents Sphinx Autodoc from generating correct types for these modules.
Therefore, Qualia introduces qualia_core.typing.TYPE_CHECKING
and qualia_codegen_core.typing.TYPE_CHECKING
constants
that always evaluate to True
when Sphinx Autodoc is running, to use in place of typing.TYPE_CHECKING
.
However, Ruff will still trigger a warning for import statements that should be in a TYPE_CHECKING
block
when Qualia’s TYPE_CHECKING
constant is used. The Ruff rule stays enabled by default as a hint for the developer to put
imports in the TYPE_CHECKING
block, but if they are already in Qualia’s TYPE_CHECKING
block, the corresponding rule (TCH001-TCH003)
may be ignored for each of them by adding for example # noqa: TCH002
at the end of the line.
Additionally, some imports for type checking may still trigger a circular import situation which would cause Sphinx Autodoc
to fail. In that case, a standard typing.TYPE_CHECKING
block should be used for the problematic import instead of Qualia’s TYPE_CHECKING
.
See Writing documentation for more information about Sphinx Autodoc, and Code style and linter for more information about the Ruff linter.
Override decorator
@override
is not a type annotation but a decorator
However it is used for typing to signify a derived class is overriding a base classe method,
and to check the signature of an overriden method in a derived class compared to the base class’ method signature.
@override
was introduced with PEP 698 as part of Python 3.12,
therefore it is not available in older Python version, and being a decorator, postponed evaluation of annotations does not help.
Usage of override in derived classes is highly recommended, and it should be imported with the following piece of code until support for the last version that does not include it is dropped:
import sys
if sys.version_info >= (3, 12):
from typing import override
else:
from typing_extensions import override
Run mypy
Make sure the typecheck
dependency group is installed, e.g.:
pdm install -G typecheck
Then run:
mypy
This will take several minutes to complete the first time using 100% of the CPU. This is expected. Subsequent runs will complete faster, but still much slower than required for real-time analysis.
Run pyright
Make sure the typecheck
dependency group is installed, e.g.:
pdm install -G typecheck
Then run:
pyright
IDE Integration
VS Code
Use the ms-pyright extension.
LunarVim
Install pyright
on your system, e.g., on ArchLinux:
sudo pacman -S pyright
Edit your LunarVim configuration (~/.config/lvim/config.lua
) to disable the built-in LSP servers and enable pyright
:
vim.list_extend(lvim.lsp.automatic_configuration.skipped_servers, { "pylyzer", "pylsp", "ruff_lsp", "pyright" })
local opts = {}
require("lspconfig")["pyright"].setup(opts)
CI/CD
After pushing new commits, the Gitlab CI/CD script (.gitlab-ci.yml
) will automatically run the mypy
as part of the check
stage,
after running tests and before uploading the Python package and updating the documentation (depending on the component).