Python Type Annotations#
Cantera employs type annotations in the Python interface primarily to support its use within third-party Python scripts and libraries, and thus aims for full type coverage of the external interface. These annotations indicate the intended types for all inputs and outputs without restricting the runtime behavior of the code.
A secondary benefit of type annotations is to facilitate static analysis of Cantera’s Python code in order to catch potential typing-related errors using tools like mypy, pyright, ty, and pyrefly. These are only able to analyze pure Python code, so they are of limited utility due to Cantera’s extensive use of Cython syntax.
Adding Type Annotations#
Annotations should be added directly to all pure Python code (.py files). For Cython
code (.pyx files), annotation support is limited and optional; instead, type stubs
(.pyi files) must be added and maintained to document the external interface. Any
changes to the externally-facing API must include explicit type annotations. Type
annotations of implementation details is optional but encouraged for pure Python code,
and may be necessary in some situations to ensure the static type checks pass.
Ensure the syntax and features support the lowest-support Python version (currently 3.12), referencing the Python and mypy documentation for guidance and current best practices. Some additional recommendations which are more specific to Cantera include:
Type aliases should generally be prepended with an underscore.
Aliases and special functions such as parametric
TypeGuards which will be used in many parts of the code should be placed in_types.py.Aliases representing any object which can be coerced into an object of type
{}should be named{}Like, in keeping withArrayLikefromNumPy.Examples:
ArrayLike,CompositionLike,_Func1Like
Prefer false positives to false negatives. In other words, if a type is too restrictive for a valid, if uncommon, usage, it should be switched for a more broad alternative.
Example: The
_TransportModelalias could be a set of string literals which enumerate all built-in transport models, but this would incorrectly flag any custom extensions. It is therefore generalized to astrtype.Exception: For input collections with a strict format it is often better to restrict the type beyond what might be accepted at runtime. For example:
A
TypedDictprovides better documentation of a dictionary of options than something likedict[str, str | float | dict[str, str | float]].For the thermodynamic state setters,
tuple[float, float, CompositionLike]is more informative than the alternativeSequence[float | CompositionLike].Clever use of
Protocols may also help to achieve optimal typing.
The following should be avoided when possible:
Use of
Anyshould be rare, and is typically reserved for dictionaries with complex or flexible contents (such askwargs).Use of
type: ignoredirectives should be considered temporary workarounds and targeted for removal in future pull requests.In the context of a pull request it is good practice to insert these as a placeholder for items where the developer requires assistance, allowing the CI job to progress and catch any other issues. Developers are encouraged to add a code comment elaborating on why it was deemed necessary.
Verifying Type Annotations#
The type-checking CI job runs a set of checks on the type annotations, and developers
should ensure these checks also pass locally prior to pushing. In this section we will
discuss each command, its purpose, and how to run it locally.
Local Setup#
Due to the use of compiled Cython code, most checks are performed on the built library rather than running directly against the source code. It is thus recommended to add the build directory to the environment variables following Using Cantera from the Build Directory:
export LD_LIBRARY_PATH=$(pwd)/build/lib
export PYTHONPATH=$(pwd)/build/python
export DYLD_LIBRARY_PATH=$(pwd)/build/lib
export PYTHONPATH=$(pwd)/build/python
$Env:PYTHONPATH = (Get-Location).Path + '\build\python'
Currently Cantera primarily uses the mypy type checker with an additional check from
pyright. They can be installed with
conda install mypy>=1.19.0 lxml pyright
or
pip install mypy[reports]>=1.19.0 pyright
Some of the optional external dependencies provide type hints which are also employed by Cantera when available, and thus must be installed for the type checks to pass:
conda install graphviz pandas pandas-stubs pint typing_extensions
Static Type Correctness#
This is the standard type-checking pass for a Python library which scans the raw source code for type-related errors such as missing annotations, attempts to access nonexistant methods/attributes, or empty collections (such as lists or dictionaries) whose type could not be inferred.
This check can be run without first building Cantera by navigating to the
interfaces/cython directory and executing:
mypy -p cantera
Which will analyze the entire package and print the results to stdout. You can also
analyze specific files with mypy [filename]. Mypy should report no issues and will
print relevant information if any are found.
When not inside the cython directory, the -p option will attempt to check the built
library instead. Note that the configuration settings for mypy are included within
the pyproject.toml file in the interfaces/cython folder, which from the
root directory can be passed in on the command line like so:
mypy --config-file interfaces/cython/pyproject.toml -p cantera
External Interface Type Coverage#
These checks examine the public interface looking for missing or underspecified annotations.
The first check uses Pyright’s --verifytypes feature to print a quick summary of the
type coverage. Ideally there will be 100% coverage with known types; however, many
types originate from external libraries which may not themselves be fully defined.
These are omitted from the results by adding the --ignoreexternal option:
pyright --ignoreexternal --verifytypes cantera
Note that this check will additionally print various static type errors prior to the
coverage report. This is expected due to differences between mypy and pyright, and
it is not necessary to address these pyright-specific errors at this time.
The second check is a more thorough coverage report, similar in format to the unit test coverage reports, generated using Mypy. This report highlights warnings and errors on a per-line basis. While the CI generates a Cobertura-format XML file, the HTML format is more straightforward for developers to view:
mypy --config-file interfaces/cython/pyproject.toml --html-report type_check/ -p cantera
This will generate a static HTML site with the index at type_check/index.html. As
with the static type check, this can also be run from interfaces/cython without first
building the code.
Runtime Type Stub Correctness#
The final category of type checks utilizes Mypy’s stubtest utility to import Cantera
and compare the runtime code against the type annotations provided by the type stubs.
Because the Cython code cannot be analyzed statically, this provides a means to verify
the stubs are both correctly implemented and cover the public interface.
Because it performs runtime analysis, this can only be run on the built library. The basic command is:
stubtest --mypy-config-file interfaces/cython/pyproject.toml \
--ignore-disjoint-bases --allowlist interfaces/cython/.mypyignore \
--concise cantera
Where the --concise option may be omitted to make stubtest print more details for
any identified issues. The --allowlist option points to a file containing a manually
curated list of methods to skip analyzing due to known and presently-unfixable errors;
for example, Cython methods have signatures which cannot be inspected at runtime, with
input parameters showing up as (*args, **kwargs).
If stubtest reports any errors and it’s not clear how to fix them, the allow list
can be automatically updated with the following commands:
# Remove everything from the allowlist except the pattern matches:
sed -i '16,$d' interfaces/cython/.mypyignore
# Generate a new allowlist and append it to the file:
stubtest --mypy-config-file interfaces/cython/pyproject.toml \
--ignore-disjoint-bases --allowlist interfaces/cython/.mypyignore \
--generate-allowlist cantera >> interfaces/cython/.mypyignore
This will ensure a consistent ordering. At present it is acceptable to add all stub
errors to the list to be addressed in later pull requests. Note that once a stub error
is fixed or otherwise rendered unnecessary, stubtest will raise an error until it is
removed from the allowlist.