Type Hints
Type hints
Why we use type hints
- We use Python 3 type hints to:
- Improve documentation
- Allow mypy to perform static checking of the code, looking for bugs
- Enforce the type checks at run-time, through automatic assertions (not implemented yet)
What to annotate with type hints
- We expect all new library code (i.e., that is not in a notebook) to have type annotations
- We annotate the function signature
- We don't annotate the variables inside a function unless mypy reports that it can't infer the type
- We strive to get no errors / warnings from the linter, including mypy
Conventions
Empty return
- Return
-> Noneif your function doesn't return - Pros:
mypychecks functions only when there is at least an annotation: so using-> Noneenables mypy to do type checking- It reminds us that we need to use type hints
- Cons:
Noneis the default value and so it might seem redundant
Invoke tasks
- For some reason
invokedoes not like type hints, so we - Omit type hints for
invoketasks, i.e. functions with the@taskdecorator -
Put
# type: ignoreso thatmypydoes not complain -
Example:
python @task def run_qa_tests( # type: ignore ctx, stage="dev", version="", ):
Annotation for kwargs
- We use
kwargs: Anyand notkwargs: Dict[str, Any] *always binds to aTuple, and**always binds to aDict[str, Any]. Because of this restriction, type hints only need you to define the types of the contained arguments. The type checker automatically adds theTuple[_, ...]andDict[str, _]container types.- Reference article
Any
Anytype hint = no type hint- We try to avoid it everywhere when possible
np.array and np.ndarray
- If you get something like the following lint:
bash dataflow/core/nodes/sklearn_models.py:537:[amp_mypy] error: Function "numpy.core.multiarray.array" is not valid as a type [valid-type] - Then the problem is probably that a parameter that the lint is related to has
been typed as
np.arraywhile it should be typed asnp.ndarray:python `x_vals: np.array` -> `x_vals: np.ndarray`
Handling the annoying Incompatible types in assignment
mypyassigns a single type to each variable for its entire scope- The problem is in common idioms where we use the same variable to store
different representations of the same data
python output : str = ... output = output.split("\n") ... # Process output. ... output = "\n".join(output) - Unfortunately the proper solution is to use different variables
python output : str = ... output_as_array = output.split("\n") ... # Process output. ... output = "\n".join(output_as_array) - Another case could be:
python from typing import Optional def test_func(arg: bool): ... var: Optional[bool] = ... dbg.dassert_is_not(var, None) test_func(arg=var) - Sometimes
mypydoesn't pick up theNonecheck, and warns that the function expects aboolrather than anOptional[bool]. In that case, the solution is to explicitly usetyping.caston the argument when passing it in, note thattyping.casthas no runtime effects and is purely for type checking. - Here're the relevant docs
- So the solution would be:
python from typing import cast ... ... test_func(arg=cast(bool, var))
Handling the annoying "None" has no attribute
- In some model classes
self._modelparameter is being assigned toNonein ctor and being set after callingset_fit_statemethod - The problem is that statically it's not possible to understand that someone
will call
set_fit_statebefore usingself._model, so when a model's method is applied:python self._model = self._model.fit(...)the following lint appears:bash dataflow/core/nodes/sklearn_models.py:155:[amp_mypy] error: "None" has no attribute "fit" - A solution is to
- Type hint when assigning the model parameter in ctor:
python self._model: Optional[sklearn.base.BaseEstimator] = None - Cast a type to the model parameter after asserting that it is not
None:python hdbg.dassert_is_not(self._model, None) self._model = cast(sklearn.base.BaseEstimator, self._model)
Disabling mypy errors
- If
mypyreports an error and you don't understand why, please ping one of the python experts asking for help - If you are sure that you understand why
mypyreports and error and that you can override it, you disable thiserror - When you want to disable an error reported by
mypy: - Add a comment reporting the
mypyerror - Explain why this is not a problem
- Add
# type: ignorewith two spaces as usual for the inline comment - Example
python # mypy: Cannot find module named 'pyannotate_runtime' # pyannotate is not always installed from pyannotate_runtime import collect_types # type: ignore
What to do when you don't know what to do
- Go to the
mypyofficial cheat sheet - Use
reveal_type - To find out what type
mypyinfers for an expression anywhere in your program, wrap it inreveal_type() mypywill print an error message with the type; remove it again before running the code- See
the official
mypydocumentation
Library without types
mypyis unhappy when a library doesn't have types- Lots of libraries are starting to add type hints now that python 2 has been
deprecated
bash *.py:14: error: No library stub file for module 'sklearn.model_selection' [mypy] - You can go in
mypy.iniand add the library (following the alphabetical order) to the list - Note that you need to ensure that different copies of
mypy.iniin different sub projects are equal ```bashvimdiff mypy.ini amp/mypy.ini or cp mypy.ini amp/mypy.ini ```
Inferring types using unit tests
- Sometimes it is possible to infer types directly from unit tests. We have used this flow to annotate the code when we switched to Python3 and it worked fine although there were various mistakes. We still prefer to annotate by hand based on what the code is intended to do, rather than automatically infer it from how the code behaves.
- Install
pyannotatebash > pip install pyannotate - To enable collecting type hints run
bash > export PYANNOTATE=True - Run
pytest, e.g., on a subset of unit tests: - Run
pytest, e.g., on a subset of unit tests likehelpers:bash > pytest helpers - A file
type_info.jsonis generated - Annotate the code with the inferred types:
bash > pyannotate -w --type-info type_info.json . --py3