Description
After lots of internal discussion, it seems that myself with some colleagues at work have concluded that the intent of NoReturn
(official docs, pep484) is ambiguous, at least as listed in the docs.
Important: I mention mypy a lot in this, however, my concern isn't with mypy, it's with the intent becoming clear within the Python official documentation. I've only added all this context in hopes of making the concerns abundantly clear.
There are 2 schools of thought. The first is that NoReturn
is only used when the function called never passes back control to the parent in the stack. This would mean all code below calling a function with NoReturn would be unreachable.
The second school of thought is that NoReturn
is used for functions which perform processing but do not explicitly return a value. This would mean that code below calling a function with NoReturn may be reachable, but you should not ever assign the value of a function with NoReturn to a variable (this would be true in both schools of thought).
The closer we thought we were to an explanation, we found information that brought doubt back in. For now we have settled on Assumption1, but the docs could easily clear this up.
Here is some demo code to explain the point
from typing import NoReturn, Any
# Assumption1: NoReturn denotes that the function in question will not pass
# control back to the calling function, making all code called below it
# unreachable
# Assumption2: NoReturn denotes that the function in question may or may not
# pass control back to the calling function, but more importantly it does not
# explicitly return anything and does not contain the return keyword or contains
# the return keyword by itself.
# Functions 1-6 pass mypy checks, comment out functions 7-8 to validate
def function1() -> NoReturn:
"""Exact behavior of scenario in official docs"""
raise RuntimeError('no way')
def function2() -> NoReturn:
"""This function should be functionally identical to function7() in that it
implicitly returns None but does not contain the return keyword. This
matches the
[mypy test implementation](https://github.com/ilevkivskyi/mypy/blob/5af9597a0a8e6b16075637145f15178130a659a1/test-data/unit/check-flags.test#L154)
and does not generate the error
`error: Implicit return in function which does not return` in mypy that
function7 does, though it is functionally identical (as far as type goes).
"""
pass
def function3() -> None:
"""This function implicitly returns None, but the return keyword is used.
If NoReturn is used here, the error
`error: Return statement in function which does not return` is generated
in mypy."""
return
def function4() -> None:
"""This function explicitly returns None"""
return None
def function5() -> NoReturn:
"""Arbitrary function created to identify if mypy had special treatment of
functions with just `pass` in them"""
pass
def function6() -> NoReturn:
"""This function should be functionally identical to function7() in that it
implicitly returns None but does not contain the return keyword. This
matches the
[mypy test implementation](https://github.com/ilevkivskyi/mypy/blob/5af9597a0a8e6b16075637145f15178130a659a1/test-data/unit/check-flags.test#L154)
and does not generate the error
`error: Implicit return in function which does not return` in mypy that
function7 does, though it is functionally identical (as far as type goes).
"""
function5()
function5()
function5()
# Function7 does not pass mypy checks and function8 calls function7, comment out to validate
def function7() -> NoReturn:
"""This function performs actions, but does not explicitly return and does
not contain the return keyword. This generates the error
`error: Implicit return in function which does not return` in mypy.
Implicitly, the returned value is None"""
1+1
def function8() -> NoReturn:
"""This function should be functionally identical to function7() in that it
implicitly returns None but does not contain the return keyword. This
matches the
[mypy test implementation](https://github.com/ilevkivskyi/mypy/blob/5af9597a0a8e6b16075637145f15178130a659a1/test-data/unit/check-flags.test#L154)
and does not generate the error
`error: Implicit return in function which does not return` in mypy that
function2 does, though it is functionally identical (as far as type goes).
"""
function7()
def testing() -> None: # This should be None if assumption1 is correct, NoReturn if assumption2 is correct
a: None = function1() # This should cause an error in all cases
b: None = function2() # This code is unreachable if assumption1 is correct. If assumption2 is correct it will cause an error like `assigning result of a function call, where the function has no return`
c: None = function3() # This code is unreachable if assumption1 is correct, and not necessarily cause an error if assumption2 is correct
x = function2 # Valid use-case assigning a function to a variable
y = function2() # Valid use-case of when the intent might have been to assign a function to a variable, but instead the value of that function was assigned
Edited: Updated example code in efforts to be more clear