home blog portfolio Ian Fisher

Pyright didn't catch my error

I was looking at some Python code I wrote a while ago, when I realized it had an obvious error:

import subprocess
from typing import Any, Tuple

def sh2(cmd: str, check: bool = True, **kwargs: Any) -> Tuple[str, str]:
    proc = _sh(check=check, capture_output=True, **kwargs)
    return proc.stdout, proc.stderr

def _sh(cmd: str, **kwargs: Any) -> subprocess.CompletedProcess[str]:
    return subprocess.run(["/usr/bin/env", "bash", "-c", cmd], text=True, **kwargs)

When sh2 calls _sh, it does not pass the cmd parameter.

It's a simple bug, so I wondered why Pyright, which I run in pre-commit, didn't catch it. My conjecture is that the presence of **kwargs fools Pyright; it thinks that kwargs could supply the cmd parameter, so the call to _sh is OK. In fact, because cmd is also a parameter to sh2, it's impossible for kwargs to contain cmd, so I don't think there's any way to invoke sh2 that doesn't trigger a TypeError. I filed an issue with Pyright: https://github.com/microsoft/pyright/issues/10933.

I also didn't get a warning that the cmd argument was unused in sh2, which would have alerted me to the type error. Pyright explicitly does not check for unused parameters, so it's up to the linter to catch them. Flake8 does not have built-in support, but there's a plugin, flake8-unused-arguments, that you can install, and then add to your flake8 config:

enable-extensions = U100

unused-arguments-ignore-abstract-functions = True
unused-arguments-ignore-overload-functions = True
unused-arguments-ignore-override-functions = True
unused-arguments-ignore-variadic-names = True

When I turned this check on, it revealed many other places where parameters were unused. Most were innocuous, but one or two also appeared to be genuine bugs.

Gradual typing is better than nothing at all, but it's not perfect. Even aside from type-checker edge cases, it's hard for me to feel confident that I haven't accidentally opted out of a crucial type-check because I used the Any type, or forgot to annotate a function's parameters, or turned off a configuration option.