Repository: python/mypy

Issue:
Incorrect "implicitly abstract" function when function returns None and implementation is 'pass'
**Bug Report**

**To Reproduce**

```python
import typing

class A(typing.Protocol):
    def require_method(self) -> None: ...

    def optional_method(self) -> None:
        pass

class B(A):
    def require_method(self) -> None:
        print("B")
    
a = B()
```

**Expected Behavior**

This is valid code. The `optional_method` is optional and is implemented with a valid do-nothing body. It doesn't make sense to `return None` from this function.

**Actual Behavior**

```
% mypy --strict file.py
file.py:13: error: Cannot instantiate abstract class "B" with abstract attribute "optional_method"
file.py:13: note: The following method was marked implicitly abstract because it has an empty function body: "optional_method". If it is not meant to be abstract, explicitly return None.
Found 1 error in 1 file (checked 1 source file)
```

**Your Environment**

- Mypy version used: 0.981
- Mypy command-line flags: --strict
- Mypy configuration options from `mypy.ini` (and other config files): None
- Python version used: 3.10.6



Discussion:
This was implemented in this patch https://github.com/python/mypy/pull/12118

It uses a weak heuristic to guess whether a method is abstract and gives no option (that I can find) to disable the guess. How about a a command line option to disable "type checks" that makes wild guesses about the author's intention?

FWIW, pyright treats `...` differently from `pass` when determining whether a protocol method is considered "implicitly abstract", so it does not produce an error for the above code. Apparently mypy treats both `...` and `pass` the same in this regard. Was this intentional? Would it make sense for mypy to treat `pass` differently here?
I think that treating `pass` as not being implicitly abstract could be reasonable. One reason for the current behavior may be that Python 2 didn't have `...` as a literal, but I'm not sure.

@ilevkivskyi @thomkeh What do you think?


> I think that treating `pass` as not being implicitly abstract could be reasonable.

Yes, I agree. The reason why I did it this way was simply that mypy already had the `is_trivial_body` function and I re-used it.

In addition to `...` and `pass`, the `is_trivial_body` function also returns `true` for function bodies that only contain a docstring and I think also those that only contain `raise NotImplementedError`: https://github.com/python/mypy/blob/a68f0a531392eef894a03597240afabbc745c0cd/mypy/semanal.py#L6313

Oh, by the way, I initially treated functions returning `None` and `Any` differently, but that functionality was removed during code review
I don't like an idea of having a flag for this. I also don't like deciding implicit abstractness depending on the return type (optional vs non-optional). We just need to agree on such a set of rules about what is implicitly abstract that would satisfy everyone.

Anecdotally, there are some post-Python 2 code bases that still have `pass` in protocols. So if we will now exclude `pass`, those will generate errors (unless return type is optional). We can of course try and see what will be the output from `mypy_primer`.
I'm pretty happy with the current rules. The error message is clear `The following method was marked implicitly abstract because it has an empty function body: "optional_method". If it is not meant to be abstract, explicitly return None.` and I don't get the big deal over doing:
```
    def optional_method(self) -> None:
        return None
```
It much more clearly communicates something is different than the difference between "..." and "pass". So I'd vote to close this issue.
To many programmers, particularly from other languages like C++, this goes against common programming idioms. Returning a value from a function typed as void/None is an error.

The ellipses are fine. Why does there need to be more heuristic than that?

Having spurious return statements in functions that are not supposed to return a value is going to lead to readability and other bugs.

I think there's an argument to be made that it is a typing error when functions declared as None return any value, even None.

> Returning a value from a function typed as void/None is an error ... typing error when functions declared as None return any value, even None

This also works:
```
    def optional_method(self) -> None:
        return
```
I'd be in favour of changing the error message to say "explicitly `return` or `return None`", if that helps address the concern.
A `return` without a value is a reasonable workaround.

I still think `pass` should also work; but I've already updated our code and this isn't a big issue for me now.

FWIW this overall feature caught a few edge cases (actual missing abstract methods) in our code that I fixed too, although they weren't actually reachable. Thanks!


Resolving patch:
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -1341,15 +1341,16 @@ def cannot_instantiate_abstract_class(
             return
         if len(attrs_with_none) == 1:
             note = (
-                "The following method was marked implicitly abstract because it has an empty "
-                "function body: {}. If it is not meant to be abstract, explicitly return None."
+                f"{attrs_with_none[0]} is implicitly abstract because it has an empty function "
+                "body. If it is not meant to be abstract, explicitly `return` or `return None`."
             )
         else:
             note = (
                 "The following methods were marked implicitly abstract because they have empty "
-                "function bodies: {}. If they are not meant to be abstract, explicitly return None."
+                f"function bodies: {format_string_list(attrs_with_none)}. "
+                "If they are not meant to be abstract, explicitly `return` or `return None`."
             )
-        self.note(note.format(format_string_list(attrs_with_none)), context, code=codes.ABSTRACT)
+        self.note(note, context, code=codes.ABSTRACT)
 
     def base_class_definitions_incompatible(
         self, name: str, base1: TypeInfo, base2: TypeInfo, context: Context


Test patch:
diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test
--- a/test-data/unit/check-protocols.test
+++ b/test-data/unit/check-protocols.test
@@ -3105,14 +3105,14 @@ class NoneCompatible(Protocol):
 
 class A(NoneCompatible): ...
 A() # E: Cannot instantiate abstract class "A" with abstract attributes "f", "g", "h", "i" and "j" \
-    # N: The following methods were marked implicitly abstract because they have empty function bodies: "f", "g", "h", "i" and "j". If they are not meant to be abstract, explicitly return None.
+    # N: The following methods were marked implicitly abstract because they have empty function bodies: "f", "g", "h", "i" and "j". If they are not meant to be abstract, explicitly `return` or `return None`.
 
 class NoneCompatible2(Protocol):
     def f(self, x: int): ...
 
 class B(NoneCompatible2): ...
 B() # E: Cannot instantiate abstract class "B" with abstract attribute "f" \
-    # N: The following method was marked implicitly abstract because it has an empty function body: "f". If it is not meant to be abstract, explicitly return None.
+    # N: "f" is implicitly abstract because it has an empty function body. If it is not meant to be abstract, explicitly `return` or `return None`.
 
 class NoneCompatible3(Protocol):
     @abstractmethod