10

I have two functions:

def get_foo(params) -> Optional[str]
def bar(foo: str)

And a function which chains these functions together:

def f(params):
    # other stuff up here
    foo = get_foo(params)
    return bar(foo)

I know based on the other things happening in my function that the result of get_foo will never be None.

When I run mypy against this file, I of course get errors:

error: Argument 1 of "bar" has incompatible type "Optional[str]"; expected "str"

which makes sense.

I could add an assert foo is not None statement, but this is hot-path code and in my tests it has measurable performance impact. I would like to make a type assertion for mypy only. How do I do that?

EDIT: I also tried adding a comment #type: str after the assignment statement, but this generated a similar error

martineau
  • 119,623
  • 25
  • 170
  • 301
Daniel Kats
  • 5,141
  • 15
  • 65
  • 102
  • 1
    A note: If you run Python with the `-O` option ("optimize"), it will remove the `assert`s at compile time, so it has no runtime penalty. Only other change it makes is to make `__debug__` a constant meaning `False` instead of `True`, so most code should run unchanged aside from the `assert`s. – ShadowRanger Sep 14 '19 at 01:23

2 Answers2

12

You're not going to be happy about this. The officially designed way to assert to static type checkers that a value has a specific type is typing.cast, which is an actual function with a real runtime cost, I believe more expensive than the assert you want to replace. It just returns its second argument unchanged, but it's still got function call overhead. Python's type annotation system is not designed with a zero-overhead type assertion syntax.

As an alternative, you could use Any as an "escape hatch". If you annotate foo with type Any, mypy should allow the bar call. Local variable annotations have no runtime cost, so the only runtime cost is an extra local variable store and lookup:

from typing import Any

def f(params):
    foo: Any = get_foo(params)
    return bar(foo)

Aside from that, your best option may be to use the assert and run Python with the -O flag, which disables asserts.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • 5
    Thanks, really appreciate the answer and I marked it as accepted. However, this does feel like a missing feature from mypy. In contrast, TypeScript allows [manually narrowing types through type assertions](https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions). However in the example above if you change `Any` to `str`, this will generate an error. – Daniel Kats Sep 14 '19 at 04:50
  • "... typing.cast, which is an actual function with a real runtime cost, I believe more expensive than the assert you want to replace..." This is not true. Per [doc](https://docs.python.org/3/library/typing.html#typing.cast), "but at runtime we intentionally don’t check anything (we want this to be as fast as possible)." – KFL Jul 28 '21 at 07:52
  • 2
    @KFL While ``cast`` doesn't do anything, it is still a function that needs to be looked up and called. Function calls, even of identity functions, are *not* free in Python. – MisterMiyagi Jul 28 '21 at 08:05
1

You can use TYPE_CHECKING variable that is False during runtime but True during type checking. This would avoid performance hit of assert:

from typing import TYPE_CHECKING

def f(params):
    # other stuff up here
    foo = get_foo(params)
    
    if TYPE_CHECKING:
        assert foo is not None
    
    return bar(foo)

herophuong
  • 354
  • 6
  • 17
  • 2
    That's still got a performance impact - you've got to look up a global variable and branch on it, both of which are much more expensive operations in Python than someone might expect if they're used to languages like C. – user2357112 Jul 28 '21 at 08:21