Help me understand NoReturn

Hi,

So I wrote a function that sometimes doesn’t return i.e. it will raise an exception under certain conditions.

from typing import NoReturn

def foo(a: int) -> int|NoReturn:
    if not is instance(a, int):
        raise TypeError("a must be int")
    return a*2

That raises error that | isn’t defined for NoReturn. Strangely I could do Union[int, NoReturn] but I’m assuming mypy doesn’t like that given the final comment on this answer

Am I doing an anti pattern here? Is it wrong to annotate that a function could raise an exception? If so wouldn’t it be useful to indicate what type of exceptions to possibly except them?

Edit: for some reason I’m remembering someone mentioning on a mailing list that annotating possible exceptions is pointless bc a function being invoked in the function in question (invoked by the invoked) could raise and it’s a rabbit hole and I guess that makes sense…maybe

I think you’re on the right track, NoReturn is not for exceptions being raised, it’s literally for functions that cannot ever return (which are uncommon if not rare).

I think you’re on the right track, NoReturn is not for exceptions being raised

It can be, if the function can only raise exceptions. This is in fact the example the mypy docs give:

More types - mypy 1.8.0 documentation

Speculation, but I suspect the commonest case for NoReturn is functions that wrap sys.exit.

In general, any function that calls Python code could raise literally any exception you have ever heard of, or even exceptions that didn’t exist before you called the function.

For example len(obj) is guaranteed to return an int, or raise TypeError, provided obj is a builtin. But as soon as obj is a custom Python class with a __len__ method, it could raise anything, deliberately or due to bugs.

Any function, builtin or not, could raise and not return if given invalid input. So every function with a return annotation should be read as “return this type, or raise”.

Since every function could, in principle, raise and not return, it would be a waste of time to annotate everything that could raise. But that is not the purpose of the NoReturn annotation:

So the purpose of NoReturn is to annotate functions that unconditionally always raise, or exit without either returning or raising.

(I guess it could also be used to annotate a function which never terminates.)

So Union[NoReturn, T] for some type T should be considered an error by mypy, I don’t know why it isn’t.

And given that unions with NoReturn should not be supported, there is no point in supporting the | operator.

3 Likes

Yeah, I’d agree with that. I’ve written a number of functions with infinite loops in them, intending to have them terminate with an exception at some point (most commonly KeyboardInterrupt or EOFError).

So Union[NoReturn, T] for some type T should be considered an error by mypy, I don’t know why it isn’t.

I’m assuming it is, given the SO post I linked. It is not an error in python unlike NoReturn|T, for some type T, which raises an error in python. Which seems inconsistent given | is just shorthand for Union

What version of Python are you using?

In 3.10, you can form unions of NoReturn with other types:


int>NoReturn

# returns typing.Union[int, typing.NoReturn]

Possibly this has changed in 3.11. If it has, it is very likely that NoReturn’s __or__ method raises an error, but calling Union directly does not.

For Union[NoReturn, T] to give an error, it would need to hard-code Union to recognise NoReturn as special. Maybe nobody has bothered to do this?

Maybe you should ask on the typing mailing list to see whether this is deliberate or just an oversight.

This was with 3.9

The | operator for types is still new. It was introduced in Python 3.10:

A new type union operator was introduced which enables the syntax X | Y . This provides a cleaner way of expressing ‘either type X or type Y’ instead of using typing.Union, especially in type hints.

Please next time copy the error message instead of describing it.


You can use this new operator in type hints with older versions of Python (as far as 3.7) if your type checker (mypy, pyright…) supports it. Just tell the Python interpreter to not evaluate the annotation expressions by putting this __future__ import at the beginning of a module:

from __future__ import annotations

https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-x-y-syntax-for-unions

Thank you. Yes I should have just copied the error. I switch between 3.7, 3.9, and 3.10 (different systems) that I get features confused.