Type checking - Pyright

Ok, so we have a linter, which creates so called AST (Abstract Syntax Tree) of the code and checks it against the rules. But it doesn’t take into account the types and which packages are installed and which are not. That’s where type checking comes in.

The problem

I’d say type checking works two fold. On one hand it checks if the types are correct, on the other it is environment aware tool that knows which packages are installed and which are not and what are their contents.

Types

The simplest example is:

def divide(a: int, b: int) -> int:
    return a / b

pyright will catch this issue:

example.py:2: error: Incompatible return value type (got "float", expected "int")  [return-value]

But a more interesting example is:

def foo(a: int) -> int | None:
    if a > 0:
        return a
    else:
        return None

def bar(a: int) -> int:
    return a + 1
x = foo(1)
y = bar(x)

We get an error:

example.py:10:9 - error: Argument of type "int | None" cannot be assigned to parameter "a" of type "int" in function "bar"
    Type "int | None" is incompatible with type "int"
      "None" is incompatible with "int" (reportArgumentType)

pyright knows that x can be None and it will catch it. What’s cool is that

x = foo(1)
if x is not None:
    y = bar(x)
else:
    y = 0

Will work just fine and pyright will know that x is an integer inside the if block.

Environment awareness

ruff will catch issue like:

import numpy as np

x = npp.array([1, 2, 3])

and it will tell you that npp is not defined.

But it won’t catch issues like:

import numpy as np

x = np.arrray([1, 2, 3])

From ruff perspective, everything is fine, we’re importing numpy and using it. Maybe it has a function called arrray? Who knows?

The solution

As you may have guessed, the solution is pyright or type checking in general (with tools like mypy/pyre).

Since Python 3.5, we have type annotations. You can add type hints to your functions and variables, but it’s not enforced by the language. Since then, many projects added detailed type hints to their codebase and you exactly know what type of data you should expect. For example we know that pd.read_csv returns a pd.DataFrame, and that df['column'] returns a pd.Series. This way IDE can provide better autocompletion.

In tapyr we use pyright for type checking, as this is the tool used by shiny internally.

How to use it

You should turn on type checking in your IDE. In VSCode you can do it by installing Python extension and then clicking here:

Turning on type checking in VS Code

Then, pyright will check your code and show you errors in the Problems tab. You can also run it from the terminal:

pyright

Type checking is also automatically run in the CI pipeline (more on that later).