Repository: python/mypy (Python)

Issue:
warn_no_return = False: handle implicit "return None" (not ignoring return type)
`warn_no_return` is on by default, but does not take into account that `None` is returned explicitly.

I think it should maybe only warn if the return type does not include `None` (i.e. uses `typing.Optional`).

Given t_mypy.py:

```python
import typing


def no_return_optional(arg) -> typing.Optional[str]:
    if arg:
        return "false"


def no_return(arg) -> str:
    if arg:
        return "false"


a1 = no_return_optional(True)
a2 = no_return_optional(False)
reveal_type(a1)
reveal_type(a2)

b1 = no_return(True)
b2 = no_return(False)
reveal_type(b1)
reveal_type(b2)
```

`mypy --warn-no-return` shows:

```
t_mypy.py:4: error: Missing return statement
t_mypy.py:9: error: Missing return statement
t_mypy.py:16: note: Revealed type is 'Union[builtins.str, None]'
t_mypy.py:17: note: Revealed type is 'Union[builtins.str, None]'
t_mypy.py:21: note: Revealed type is 'builtins.str'
t_mypy.py:22: note: Revealed type is 'builtins.str'
Found 2 errors in 1 file (checked 1 source file)
```

`mypy --no-warn-no-return` does not show any error, but should warn/error about the missing return statement / wrong return value for `no_return` (which is annotated to return `str`, but might return `None`).

mypy 0.730+dev.28423cd78062b81365ec9aaa591b4e823af1606d


Hints:
This is by design. `warn_no_return` excepts an explicit return statement even in functions that return an optional type. The assumption is that relying on the implicit `None` return is often an error (or at least a code smell).

When the check is disabled, mypy effectively behaves as if there Python had no implicit `None` return. I can see how this can be surprising, and I may be persuaded that this is not the best way to implement this.

At the very least we could make the documentation more detailed.
> When the check is disabled, mypy effectively behaves as if there Python had
> no implicit `None` return. I can see how this can be surprising, and I may be
> persuaded that this is not the best way to implement this.

The main problem I see here is that you are forced to use the default
(`warn_no_return = True`), since otherwise you are not getting an error with:

```python
def f(a) -> int:
    if a:
        return 1
```

Disabling the warning is useful/necessary to not change code unnecessarily
when adding type annotations, but currently you are forced to add `return None` at the end always despite of the setting to not have errors go silent 
(btw: `return` by itself results in `"Return value expected"`).

Therefore I think it would be great if the behavior in this regard could be
changed (as if `return None` was used), since otherwise disabling the warning
also hides real errors silently.
This would affect the non-default part of the option.


While I'd still prefer to have `Optional[…]` not cause a warning without an explicit `return None` by default, I think the above is more important for now, and have changed/clarified the issue title in this regard.

Docs for reference: https://mypy.readthedocs.io/en/latest/command_line.html#cmdoption-mypy-no-warn-no-return
FWIW, I am fine with the current behaviour -- "explicit is better than implicit". The error message and the docs however can be improved. 
@ilevkivskyi 
Don't you agree that mypy not showing an error with the above example (with `warn_no_return = False`) is wrong / hiding the error?
I don't see a problem if someone opts-in to unsafety and gets an unsafety. (Also this is my final opinion here, I don't have time to write an essay about why/what/etc.)
Ok, fair enough - I do not want to waste anyone's time of course.
But just to be clear - the option is not named `ignore_errors_with_no_return`, i.e. it is not clear but surprising that it behaves that way.
I also think that mypy ignoring the fact that Python has implicit returns is a design decision to be revisited, and that should be done before adding a warning to the docs then probably.
> I also think that mypy ignoring the fact that Python has implicit returns is a design decision to be revisited

I agree wholeheartedly. From https://github.com/python/mypy/issues/3974#issuecomment-331648044:

> This is a style issue

Indeed, Mypy is overreaching here and enforcing a stylistic choice at the cost of correct language semantics.

This is the only reason I'm not using Mypy 😢 
The most likely way around this that I can see is adding support for enabling/disabling individual error codes, and giving the relevant error message a specific error code that can be disabled (which would hide all these messages). Now anybody who doesn't like this error can disable the relevant error code.

I'm happy to accept a PR that implements enabling/disabling error codes (we should first discuss how this would be configured in a separate issue).
@JukkaL Thanks! I opened https://github.com/python/mypy/issues/8975
There's actually a few scenarios relevant here:

## Scenario 1 

```python
def f1(a) -> int:
    if a:
        return 1
```
- Currently, this fails with `error: Missing return statement`.
- This makes sense, the function **is** missing a return.

## Scenario 2

```python
def f2(a) -> Optional[int]:
    if a:
        return 1
```

- Currently, this fails with: `error: Missing return statement`.
- This code is fine, it explicitly states that it returns `None`.

## Scenario 3

```python
def f3(a) -> Optional[int]:
    if a:
        return 1
    return None
```

-  This one is doubly explicit, and passes fine.

## Scenario 4

```python
def f4(a) -> None:
    pass
```
- This looks like scenario 2 (`None` returned implicitly).
- However, this one passes checks fine.
- Scenario 4 is because of a mypy exception for functions with a one-line body. These are used as stubs and we found usability issues around this in real-world code. Arguably this behavior should not be the default (except for stub files), and we could introduce a flag to enable it.

- The error you get in Scenario 2 enforces a PEP 8 rule that is dear to my heart -- if you return a value, you must *explicitly* return a value on all code paths. Even if that value has an `Optional` type. The quickest way to avoid the error is to add `return None` at the end. If you don't like that, maybe use `--no-warn-no-return`.
My issue here is that I have to be explicit twice. I'm more of the DRY mentality:

Initial code:
```python
def f2(a) -> int:
    if a:
        return 1
```

Be explicit about the return value possibly being `None`:
```python
def f2(a) -> Optional[int]:
    if a:
        return 1
```

Be explicit about the return value possibly being `None` **again**:
```python
def f2(a) -> Optional[int]:
    if a:
        return 1
    return None
```

It's very annoying that 1/3 of my function is now just redundant code that does nothing. It just hurts readability. It was obvious from the start it could return `None`. I can respect having to be explicit about it by adding `Optional`. Having to be explicit about it **twice** it just noise.
Whatever.
Note there is now support for enabling/disabling error codes https://github.com/python/mypy/pull/9172 which will be released soon https://github.com/python/mypy/issues/9290

I'm not sure what this code would be though
While I agree in general with the Zen of Python,
I feel that warning in this particular case represents feature creep for `mypy`.

Specifically, `mypy` is a type checker, and should (in my opinion) allow whatever silly, broken or ugly code someone may write, as long as types are correct. It's a job for other tools to enforce PEP-8 or local style conventions. 
I once read a beautiful code styling principle on some forum. It goes like this:
`I hate code. I want as little of it as possible.`

Reading this affected my code styling in a very profound way since then.
I had a quick go at using the new return codes functionality (https://github.com/python/mypy/issues/8975) to get the behavior requested by OP. I.e. to accept an implicit `return None` for a function returning an `Optional` type, but throw an error if the function needs to return a type that is not `Optional`.

Unfortunately it seems that both situations result in the same `return` error code. In other words, setting `--disable-error-code return` simply replicates the behavior of `--no-warn-no-return`, which takes us back to the original issue. Has anyone else figured out a better approach here, i.e. one which which distinguishes between a valid implicit return and an invalid one? I suppose that more granular error codes need to be implemented in order for this to be resolved?
If your code works and you are confident about it, although mypy is nagging you about not having an explicit return statement in a function/method, just ignore mypy. What mypy is giving you are only suggestions (which you can ignore); it's not like mypy is giving you messages about critical security vulnerabilities and you must make your code mypy compliant and follow its suggestions to the teeth or else you will die on the spot. Having your code mypy-friendly is just like having your code PEP8-friendly. Who among you follow PEP8 to the teeth? Just have code that you are happy with and that it does exactly what you need it to do.
@PedanticHacker that's a reasonable approach in principle, though not always implementable in practice. Specifically, we're making a push to integrate mypy checks into our CI/CD pipelines. Which means that the mypy checks need to pass, otherwise "computer says no" and the code doesn't get deployed.

Just like others who commented above, we'd love to have a way for mypy to check and enforce typing, without enforcing style choices. We experimented with adding explicit `return`s everywhere simply to satisfy mypy, but most members of our dev team were ultimately not keen on them. Anyway, that's neither here nor there. At this point the best solution we can hope for would be to have the above-mentioned granularity in the error codes. Based on [the docs](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-function-returns-a-value-return) it's not currently there.

For the time being we've added `--disable-error-code return` to our CI tests (_#YOLO_). Finally, for anyone looking for a workaround of sorts - the type-checker built into Pylance (VSCode's linter, which I think is basically [pyright](https://github.com/microsoft/pyright)) doesn't mind implicit `return`s where allowed, but does throw errors if the return type isn't `Optional`.
I understand.
Normally I'm in agreement with GvR on this. I find that implicitly returning `None` leads to confusion and extra mental burden, more often than it leads to cleanliness or elegance.

However, I can think of one exception:

```python
import sys

def main() -> Optional[int]:
    if something_bad:
        return 1
    print('ok :)')

if __name__ == '__main__':
    sys.exit(main())
```

In this case, it would be nice to selectively recognize the implicit `return None`, without disabling type checking of the return value. Mostly for aesthetic reasons, including symmetry with the behavior of C 99 `main()` (you can omit the `return 0`).
Hi,
so, is this fixed? 
Is there a way of ONLY disabling implicit None returns but forcing every other return check? 

I believe this must be fixed because if the semantics of the language are that function/methods which don't return anything return None by default, then my types are correct even if I avoid explicit None returns. 

I understand that mypy must check all paths to make sure to comply with types, hence it is correct to warn when a path is missing a return statement. But in the None case, the return statment is there but hidden. 

To me it's clearly a Mypy error (or feature creep) just because of this logic and I don't think it can be denied.

Thanks

Patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -1217,7 +1217,7 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
                     self.accept(item.body)
                 unreachable = self.binder.is_unreachable()
 
-            if self.options.warn_no_return and not unreachable:
+            if not unreachable and not body_is_trivial:
                 if defn.is_generator or is_named_instance(
                     self.return_types[-1], "typing.AwaitableGenerator"
                 ):
@@ -1228,17 +1228,29 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
                     return_type = self.get_coroutine_return_type(self.return_types[-1])
                 else:
                     return_type = self.return_types[-1]
-
                 return_type = get_proper_type(return_type)
-                if not isinstance(return_type, (NoneType, AnyType)) and not body_is_trivial:
-                    # Control flow fell off the end of a function that was
-                    # declared to return a non-None type and is not
-                    # entirely pass/Ellipsis/raise NotImplementedError.
-                    if isinstance(return_type, UninhabitedType):
-                        # This is a NoReturn function
-                        self.fail(message_registry.INVALID_IMPLICIT_RETURN, defn)
-                    else:
-                        self.fail(message_registry.MISSING_RETURN_STATEMENT, defn)
+
+                if self.options.warn_no_return:
+                    if not isinstance(return_type, (NoneType, AnyType)):
+                        # Control flow fell off the end of a function that was
+                        # declared to return a non-None type and is not
+                        # entirely pass/Ellipsis/raise NotImplementedError.
+                        if isinstance(return_type, UninhabitedType):
+                            # This is a NoReturn function
+                            self.fail(message_registry.INVALID_IMPLICIT_RETURN, defn)
+                        else:
+                            self.fail(message_registry.MISSING_RETURN_STATEMENT, defn)
+                else:
+                    # similar to code in check_return_stmt
+                    self.check_subtype(
+                        subtype_label="implicitly returns",
+                        subtype=NoneType(),
+                        supertype_label="expected",
+                        supertype=return_type,
+                        context=defn,
+                        msg=message_registry.INCOMPATIBLE_RETURN_VALUE_TYPE,
+                        code=codes.RETURN_VALUE,
+                    )
 
             self.return_types.pop()
 


Test patch:
diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test
--- a/test-data/unit/check-flags.test
+++ b/test-data/unit/check-flags.test
@@ -410,6 +410,44 @@ async def h() -> NoReturn:  # E: Implicit return in function which does not retu
 [builtins fixtures/dict.pyi]
 [typing fixtures/typing-async.pyi]
 
+[case testNoWarnNoReturn]
+# flags: --no-warn-no-return --strict-optional
+import typing
+
+def implicit_optional_return(arg) -> typing.Optional[str]:
+    if arg:
+        return "false"
+
+def unsound_implicit_return(arg) -> str:  # E: Incompatible return value type (implicitly returns "None", expected "str")
+    if arg:
+        return "false"
+
+def implicit_return_gen(arg) -> typing.Generator[int, None, typing.Optional[str]]:
+    yield 1
+
+def unsound_implicit_return_gen(arg) -> typing.Generator[int, None, str]:  # E: Incompatible return value type (implicitly returns "None", expected "str")
+    yield 1
+[builtins fixtures/dict.pyi]
+
+[case testNoWarnNoReturnNoStrictOptional]
+# flags: --no-warn-no-return --no-strict-optional
+import typing
+
+def implicit_optional_return(arg) -> typing.Optional[str]:
+    if arg:
+        return "false"
+
+def unsound_implicit_return(arg) -> str:
+    if arg:
+        return "false"
+
+def implicit_return_gen(arg) -> typing.Generator[int, None, typing.Optional[str]]:
+    yield 1
+
+def unsound_implicit_return_gen(arg) -> typing.Generator[int, None, str]:
+    yield 1
+[builtins fixtures/dict.pyi]
+
 [case testNoReturnImportFromTyping]
 from typing import NoReturn
 
diff --git a/test-data/unit/check-inline-config.test b/test-data/unit/check-inline-config.test
--- a/test-data/unit/check-inline-config.test
+++ b/test-data/unit/check-inline-config.test
@@ -114,8 +114,8 @@ import a
 [file a.py]
 # mypy: no-warn-no-return
 
-from typing import List
-def foo() -> List:
+from typing import Optional, List
+def foo() -> Optional[List]:
     20
 
 [file b.py.2]