Type checking only external APIs?

I’m one of those project maintainers that doesn’t want to use typing. But I am OK with putting some type hints on a package’s public API – particularly return types so that fancy IDEs give good completions on the outputs. When I do, I keep the bar low, only type hinting things that seem simple (numbers, strings) and leave anything non-trivial (numpy array-like types, large unions, anything protocol related) untyped.

Despite sticking to what I assumed were the simple and obvious types, I still make mistakes and find people having to # type: ignore my counterproductive attempts at being helpful. So I’m looking for as low-noise a way of checking only my own types as possible.

What I thought would do that really effectively is to run a type checker on my test suite. Since the tests exercise all intended usages of the API, they should fail if its type hints are invalid or block a legitimate usage.

A reduced example of what I want to catch and not catch:
# foo.py
import os
import numbers

# Type reasignment upsets type checkers but I consider it OK.
# I want type checkers to ignore this.
_seed = os.environ.get("FOO_HASH_SEED", "")
if _seed:
    _seed = int(_seed)
else:
    _seed = int.from_bytes(os.urandom(4), "little")

# numbers.Number isn't a valid type hint. Tests that call this function should
# fail type checking.
def add_1(x: numbers.Number):
    return x + 1
# test_foo.py
import foo

def test_add_1():
    assert foo.add_1(2) == 3  # <-- should fail

But I can’t get the filtering right. mypy’s filtering rules are apparently either:

  • Recurse imports to fetch type information from those module AND type check them.
  • Treat the whole imported module as a typing.Any free for all.

Really I want a middle ground where type information is fetched but type errors within those imports are ignored.

# --exclude doesn't exclude errors from foo.py. Checker is too fussy
> mypy --disable-error-code=import-untyped --exclude=foo.py test_foo.py 
foo.py:8: error: Incompatible types in assignment (expression has type "int", variable has type "str")  [assignment]
foo.py:10: error: Incompatible types in assignment (expression has type "int", variable has type "str")  [assignment]
foo.py:15: error: Unsupported operand types for + ("Number" and "int")  [operator]
Found 3 errors in 1 file (checked 1 source file)

# --skip skips too much. Checker ignores what I want it to find
> mypy --disable-error-code=import-untyped --follow-imports=skip test_foo.py 
Success: no issues found in 1 source file

Is anyone able to set me on the right path here? I’m surely not the only person who’s wanted to (partially) support typing externally without having to engage with it everywhere else. I know I could # type: ignore all over the (to me) false positives in my source code I’d sooner ditch my type hints entirely.

1 Like

For the example shown you could use

$ mypy --local-partial-types --allow-redefinition-new test_foo.py 
foo.py:16: error: Unsupported operand types for + ("Number" and "int")  [operator]
Found 1 error in 1 file (checked 1 source file)

That might not be quite were you were after but those flags allow reassigning different types for something unannotated. Docs for the flags are here.

1 Like

You can use ignore_errors to do this.

Either a) add a mypy config file and use a per-module ignore_errors setting or b) add `# mypy: ignore-errors` to the top of the Python file

1 Like

I’ve realised most of my confusion has come from just misreading the mypy output.

foo.py:15: error: Unsupported operand types for + (“Number” and “int”) [operator]

I saw the word Number in this message and my brain assumed it was refering to the function call in test_foo.py despite the reported file being foo.py and the message saying addition is involved.

So of course all my attempts to hide messages from foo.py made it disappear.

I need --check-untyped-defs to ensure the error I wanted appears. Then it seems either --follow-imports=silent or ignore_errors can be used to ignore errors in the main code.

> mypy --check-untyped-defs --disable-error-code=import-untyped --follow-imports=silent test_foo.py
test_foo.py:4: error: Argument 1 to "add_1" has incompatible type "int"; expected "Number"  [arg-type]
test_foo.py:4: note: Types from "numbers" aren't supported for static type checking
test_foo.py:4: note: See https://peps.python.org/pep-0484/#the-numeric-tower
test_foo.py:4: note: Consider using a protocol instead, such as typing.SupportsFloat
Found 1 error in 1 file (checked 1 source file)

Thanks for the help!

1 Like