Repository: python/mypy (Python)

Issue:
Should concrete implementations be required to implement protocol?
* Are you reporting a bug, or opening a feature request?

Bug

* Please insert below the code you are checking with mypy,
  or a mock-up repro if the source is private. We would appreciate
  if you try to simplify your case to a minimal repro.

```python

from typing import Protocol


class Person(Protocol):
    first_name: str

    def greeting(self, message: str) -> str:
        ...


class FrenchPerson(Person):
    prenom: str

    def __init__(self, name: str):
        self.prenom = name

    def salutation(self, message: str) -> str:
        return f"{message} {self.prenom}"


def print_person(person: Person) -> str:
    return person.greeting(message="Je m'appelle")


def main():
    fp = FrenchPerson(name='Henri')
    response = print_person(fp)
    print(response)
```

* What is the actual behavior/output?

PEP 544 says you can state that an implementation supports a protocol by
[subclassing the protocol](https://www.python.org/dev/peps/pep-0544/#explicitly-declaring-implementation).

It also says: "If one omits Protocol in the base class list, this would
be a regular (non-protocol) class that must implement Sized."

* What is the behavior/output you expect?

I would hope `mypy` would, either at declaration time or in the `print_person` function, spot that something claimed to be a `Person` but had neither the protocol variable `name` nor the method `greeting`.

It's also a shame that `FrenchPerson` got the `greeting` method added to it from the subclassing.

* What are the versions of mypy and Python you are using?

`mypy` 0.770 and Python 3.8.1

* What are the mypy flags you are using? (For example --strict-optional)

None.

Should concrete implementations be required to implement protocol?
* Are you reporting a bug, or opening a feature request?

Bug

* Please insert below the code you are checking with mypy,
  or a mock-up repro if the source is private. We would appreciate
  if you try to simplify your case to a minimal repro.

```python

from typing import Protocol


class Person(Protocol):
    first_name: str

    def greeting(self, message: str) -> str:
        ...


class FrenchPerson(Person):
    prenom: str

    def __init__(self, name: str):
        self.prenom = name

    def salutation(self, message: str) -> str:
        return f"{message} {self.prenom}"


def print_person(person: Person) -> str:
    return person.greeting(message="Je m'appelle")


def main():
    fp = FrenchPerson(name='Henri')
    response = print_person(fp)
    print(response)
```

* What is the actual behavior/output?

PEP 544 says you can state that an implementation supports a protocol by
[subclassing the protocol](https://www.python.org/dev/peps/pep-0544/#explicitly-declaring-implementation).

It also says: "If one omits Protocol in the base class list, this would
be a regular (non-protocol) class that must implement Sized."

* What is the behavior/output you expect?

I would hope `mypy` would, either at declaration time or in the `print_person` function, spot that something claimed to be a `Person` but had neither the protocol variable `name` nor the method `greeting`.

It's also a shame that `FrenchPerson` got the `greeting` method added to it from the subclassing.

* What are the versions of mypy and Python you are using?

`mypy` 0.770 and Python 3.8.1

* What are the mypy flags you are using? (For example --strict-optional)

None.



Hints:
I ran into exactly this today. Mypy doesn't recognize the problem. It should!
Yeah, this behavior is not what one would expect. Note that if `main` was type checked (with a `-> None` annotation), the missing attribute definition would be caught when constructing an instance of `FrenchPerson`. However, we should probably report the missing attribute already in the class definition.

There is a question about when exactly should we require a method to be implemented in the concrete derived class. The method `greeting` has a body, but since it's `...`, it should not be treated as a default implementation, I think.

@ilevkivskyi What do you think?
This behavior seems to be as-expected according to the PEP.

> I would hope mypy would, either at declaration time or in the `print_person` function, spot that something claimed to be a Person but had neither the protocol variable `first_name` nor the method `greeting`.

I don't see any reason for a type error in the class declaration or the `print_person()` function.  MyPy will correctly raise an error when you try to _instantiate_ `FrenchPerson` due to the missing `first_name` property.

    error: Cannot instantiate abstract class 'FrenchPerson' with abstract attribute 'first_name'

> It's also a shame that `FrenchPerson` got the `greeting` method added to it from the subclassing.

That's expected due to how class inheritance works.  If you want to make `Person.greeting()` abstract, annotate the method with `@abstractmethod` and optionally raise a `NotImplementedError` in the body.

> The method `greeting` has a body, but since it's `...`, it should not be treated as a default implementation, I think.

That's how it will be treated at runtime, so the type checker should reflect this.  Maybe there could be a warning if the body is `...` for a non-abstract method, as this is likely to indicate programmer error.
> I don't see any reason for a type error in the class declaration or the `print_person()` function. MyPy will correctly raise an error when you try to _instantiate_ `FrenchPerson` due to the missing `first_name` property.

When the PEP says: _"To explicitly declare that a certain class implements a given protocol, it can be used as a regular base class. "_ ... IMO, it's a statement about the _class_, not the instance. 

If it's about the instance, then that doesn't fit (again, IMO) with the zen of Python type hinting. Why warn me about a function's return type not matching the type of the `return`, and not just say "I'm not going to help until you use the function somewhere."

But I recognize that's a small part of the PEP and I doubt it really reflects the intent. I confess I'm trying to shoe-horn `zope.interface` into protocols. 

It seems like with the current behavior, a class that extends a protocol class without implementing its abstract members is treated like an abstract class.  Defining an abstract class isn't a type error, but attempting to instantiate one is.

If MyPy were to require protocol subclasses to implement all abstract protocol members, this would rule out defining an abstract subclass of a protocol, other than a subprotocol.

```
class AbstractPerson(Person):
    @abstractmethod
    def do_something(self) -> None:
        raise NotImplementedError
```

I can see a couple potential issues with this.

1. It might break type-checking for existing code that uses the protocols defined in the `typing` module.

2. It imposes a restriction on protocol subclasses at type-checking time which has no basis in actual runtime behavior.

One possible solution would be if there were a way to explicitly mark a class as non-abstract, which would signal to the type-checker that all abstract members should be implemented.  (This could be useful for ABCs as well.)

For practical purposes, you can do something like this (with or without explicitly subclassing the protocol):
```
if typing.TYPE_CHECKING:
    # Check that FrenchPerson is a concrete implementation of the Person protocol
    _: Person = FrenchPerson()
```
Note, however, that the PEP explicitly discourages this pattern.
I also ran into this testing out the following snippet:

```python
class A(Protocol):
    def a(self) -> int: ...

class B(A): ...

b = B()
b.a()
```

mypy sees no problem with this, which is wrong. At runtime, `b.b()` returns `None`, not an `int`.

Adding an incorrect implementation of `b` in the `B` implementation class is also not correctly checked. Changing out `B` for the following also passes mypy.

```python
class B(A):
    def b(self):
        return "not an int"
```
@ktbarrett Your first example passes type-checking due to a quirk of MyPy unrelated to protocols.  MyPy doesn't warn about methods/functions whose body is `...` or `pass`.  See 
#9772 for more discussion.  So `A.a` passes type-checking, and `B` inherits this method.

In your second example it looks like you meant to override `a` in the protocol.  Also, since the method is not annotated, it is considered untyped and ignored by default.  If you change the method name to `a` and annotate it, MyPy will detect errors as expected.  This is the same regardless of whether `A` is a protocol.
> due to a quirk of MyPy unrelated to protocols. MyPy doesn't warn about methods/functions whose body is ... or pass

That's quite the "quirk".

> In your second example it looks like you meant to override a in the protocol

I did. Transliteration error.

> since the method is not annotated, it is considered untyped and ignored by default ...  This is the same regardless of whether A is a protocol.

Ugh, mypy complains if you provide an incompatible type in a subclass, but just ignores the definition in the base class if you don't annotate? That's not consistent or intuitive IMO. I really like how all these "expected" corner cases team up and seemingly misbehave. There are FAR too many gotchas in mypy. It should be convention to avoid them.
There were two previous issues for the same problem: #8005 and #8395 but this one has the most discussion, so it probably makes sense to close the other two and keep this one.

I was thinking that a method in a protocol should be treated as abstract if all of the following holds:

1.    The function body only consists of `pass`, `...`, and/or a docstring.
2.    The definition of the protocol is not from a stub file (because stubs never have function bodies).
3.    Return type is not `None` or `Any` (because for those return types, an empty function body is valid).
4.    Function is not annotated with `@overload`.

You could extend the first rule to also cover functions that only raise a `NotImplementedError` but this maybe gets tricky.
This is not a good idea. [The relevant part of PEP544](https://www.python.org/dev/peps/pep-0544/#explicitly-declaring-implementation).

1. abstract methods should be marked with `abstractmethod`. All the examples in the PEP use it.
2. Protocols, like ABCs, might only be partially implemented by a class, so really this should only be checked upon instantiation, or if the class is marked final (I don't remember off the top of my head if this is supported yet). The PEP states "The semantics of `@abstractmethod` is not changed" in the second sentence of the last paragraph.
3. Method implementations are already checked for compatibility

I think the primary issue from the OP, myself, and others is the assumption of the *opposite* of 2. You see this in other languages like Java's interfaces and Rust's traits when you "implement" them. However, inheritance here isn't "implementing" the interface, it's more like using the Protocol as an ABC. ABCs allow for partial implementation. Deciding that when inheriting from Protocol the user *must* fully implement the interface will likely break a lot of users and changes the semantics of abstractmethod, violating the PEP. You could argue that the semantics of abstractmethod should have been different, which would make this change valid, but I think that ship sailed long ago...

Just for reference, I recently implemented this feature in pyright in response to user requests, and pyright users have been happy with the functionality.

My implementation uses the following rules. If these conditions are met, an error is generated for the child class declaration.
1. A class is not a protocol class but explicitly derives from a base class that is a protocol class. 
2. The base class is not declared in a stub file
3. The base class contains a method whose implementation consists only of docstrings and/or `...`

It also checks for the case where the base class contains an instance variable or class variable declaration without a value assignment and the child class also assigns no value for that variable.

FWIW, pyright doesn't treat a `pass` statement as an unimplemented method. It also doesn't ignore methods with `@overload` signatures or methods with `None` or `Any` return type annotations.

![Screen Shot 2022-02-04 at 9 11 09 AM](https://user-images.githubusercontent.com/7040122/152572695-76627dc2-ec60-446d-9666-586c27dc736f.png)

This has been implemented in pyright for about a month now, and we haven't received any negative feedback so far. I think that's evidence that this check will not break users. 

It would be atypical to subclass from a protocol and leave some members unimplemented. In the rare case where that's desired and intended, it would be appropriate to mark the subclass as a protocol also, simply by including `Protocol` in its list of base classes. This will eliminate the error condition and prevent the subclass from being instantiated without the type checker emitting an error.
Ehhhh. I'm not sure about the trivial body being implicitly abstract part. Protocol being an ABCMeta and `@abstractmethod` being used throughout the PEP imply that was the intended mechanism to mark non-implemented Protocol methods. I also don't like the fact that it doesn't apply to ABCs as well. Protocols and ABCs are essentially the same thing during defintion, so making them different makes me 😬. I think that's a slightly different concern than the OP.

If you haven't seen anyone break because of the change to check Protocol immediate subclasses, it's a good idea in my book. I might extend the functionality a bit:

> When inheriting from Protocols, if the subclass is intended to be a Protocol, it must be explicitly marked as such. If the subclass is intended to be an ABC, it must be explicitly marked as such. Otherwise it's treated as a concrete type. Concrete subclasses of Protocols must implement all abstract members. 

I'd really like to see the last sentence applied to subclasses of ABCs as well;  "Concrete subclasses or ABCs must implement all abstract members". This would make the rules between the two universal: Protocol and ABC being two different ways of saying abstract, and all other classes being concrete. I really don't like implicitly abstract classes. I want to be able to look at the class definition and see immediately if it's abstract or not.

If we can't do both, I'm less sure about it being a good idea. One should be like the other. Since `@abstractmethod` and `ABC` currently allow the definition of implicitly abstract classes at both runtime and in mypy, Protocol should behave the same way. But I guess that isn't my call.
I ran into exactly this today. Mypy doesn't recognize the problem. It should!
Yeah, this behavior is not what one would expect. Note that if `main` was type checked (with a `-> None` annotation), the missing attribute definition would be caught when constructing an instance of `FrenchPerson`. However, we should probably report the missing attribute already in the class definition.

There is a question about when exactly should we require a method to be implemented in the concrete derived class. The method `greeting` has a body, but since it's `...`, it should not be treated as a default implementation, I think.

@ilevkivskyi What do you think?
This behavior seems to be as-expected according to the PEP.

> I would hope mypy would, either at declaration time or in the `print_person` function, spot that something claimed to be a Person but had neither the protocol variable `first_name` nor the method `greeting`.

I don't see any reason for a type error in the class declaration or the `print_person()` function.  MyPy will correctly raise an error when you try to _instantiate_ `FrenchPerson` due to the missing `first_name` property.

    error: Cannot instantiate abstract class 'FrenchPerson' with abstract attribute 'first_name'

> It's also a shame that `FrenchPerson` got the `greeting` method added to it from the subclassing.

That's expected due to how class inheritance works.  If you want to make `Person.greeting()` abstract, annotate the method with `@abstractmethod` and optionally raise a `NotImplementedError` in the body.

> The method `greeting` has a body, but since it's `...`, it should not be treated as a default implementation, I think.

That's how it will be treated at runtime, so the type checker should reflect this.  Maybe there could be a warning if the body is `...` for a non-abstract method, as this is likely to indicate programmer error.
> I don't see any reason for a type error in the class declaration or the `print_person()` function. MyPy will correctly raise an error when you try to _instantiate_ `FrenchPerson` due to the missing `first_name` property.

When the PEP says: _"To explicitly declare that a certain class implements a given protocol, it can be used as a regular base class. "_ ... IMO, it's a statement about the _class_, not the instance. 

If it's about the instance, then that doesn't fit (again, IMO) with the zen of Python type hinting. Why warn me about a function's return type not matching the type of the `return`, and not just say "I'm not going to help until you use the function somewhere."

But I recognize that's a small part of the PEP and I doubt it really reflects the intent. I confess I'm trying to shoe-horn `zope.interface` into protocols. 

It seems like with the current behavior, a class that extends a protocol class without implementing its abstract members is treated like an abstract class.  Defining an abstract class isn't a type error, but attempting to instantiate one is.

If MyPy were to require protocol subclasses to implement all abstract protocol members, this would rule out defining an abstract subclass of a protocol, other than a subprotocol.

```
class AbstractPerson(Person):
    @abstractmethod
    def do_something(self) -> None:
        raise NotImplementedError
```

I can see a couple potential issues with this.

1. It might break type-checking for existing code that uses the protocols defined in the `typing` module.

2. It imposes a restriction on protocol subclasses at type-checking time which has no basis in actual runtime behavior.

One possible solution would be if there were a way to explicitly mark a class as non-abstract, which would signal to the type-checker that all abstract members should be implemented.  (This could be useful for ABCs as well.)

For practical purposes, you can do something like this (with or without explicitly subclassing the protocol):
```
if typing.TYPE_CHECKING:
    # Check that FrenchPerson is a concrete implementation of the Person protocol
    _: Person = FrenchPerson()
```
Note, however, that the PEP explicitly discourages this pattern.
I also ran into this testing out the following snippet:

```python
class A(Protocol):
    def a(self) -> int: ...

class B(A): ...

b = B()
b.a()
```

mypy sees no problem with this, which is wrong. At runtime, `b.b()` returns `None`, not an `int`.

Adding an incorrect implementation of `b` in the `B` implementation class is also not correctly checked. Changing out `B` for the following also passes mypy.

```python
class B(A):
    def b(self):
        return "not an int"
```
@ktbarrett Your first example passes type-checking due to a quirk of MyPy unrelated to protocols.  MyPy doesn't warn about methods/functions whose body is `...` or `pass`.  See 
#9772 for more discussion.  So `A.a` passes type-checking, and `B` inherits this method.

In your second example it looks like you meant to override `a` in the protocol.  Also, since the method is not annotated, it is considered untyped and ignored by default.  If you change the method name to `a` and annotate it, MyPy will detect errors as expected.  This is the same regardless of whether `A` is a protocol.
> due to a quirk of MyPy unrelated to protocols. MyPy doesn't warn about methods/functions whose body is ... or pass

That's quite the "quirk".

> In your second example it looks like you meant to override a in the protocol

I did. Transliteration error.

> since the method is not annotated, it is considered untyped and ignored by default ...  This is the same regardless of whether A is a protocol.

Ugh, mypy complains if you provide an incompatible type in a subclass, but just ignores the definition in the base class if you don't annotate? That's not consistent or intuitive IMO. I really like how all these "expected" corner cases team up and seemingly misbehave. There are FAR too many gotchas in mypy. It should be convention to avoid them.
There were two previous issues for the same problem: #8005 and #8395 but this one has the most discussion, so it probably makes sense to close the other two and keep this one.

I was thinking that a method in a protocol should be treated as abstract if all of the following holds:

1.    The function body only consists of `pass`, `...`, and/or a docstring.
2.    The definition of the protocol is not from a stub file (because stubs never have function bodies).
3.    Return type is not `None` or `Any` (because for those return types, an empty function body is valid).
4.    Function is not annotated with `@overload`.

You could extend the first rule to also cover functions that only raise a `NotImplementedError` but this maybe gets tricky.
This is not a good idea. [The relevant part of PEP544](https://www.python.org/dev/peps/pep-0544/#explicitly-declaring-implementation).

1. abstract methods should be marked with `abstractmethod`. All the examples in the PEP use it.
2. Protocols, like ABCs, might only be partially implemented by a class, so really this should only be checked upon instantiation, or if the class is marked final (I don't remember off the top of my head if this is supported yet). The PEP states "The semantics of `@abstractmethod` is not changed" in the second sentence of the last paragraph.
3. Method implementations are already checked for compatibility

I think the primary issue from the OP, myself, and others is the assumption of the *opposite* of 2. You see this in other languages like Java's interfaces and Rust's traits when you "implement" them. However, inheritance here isn't "implementing" the interface, it's more like using the Protocol as an ABC. ABCs allow for partial implementation. Deciding that when inheriting from Protocol the user *must* fully implement the interface will likely break a lot of users and changes the semantics of abstractmethod, violating the PEP. You could argue that the semantics of abstractmethod should have been different, which would make this change valid, but I think that ship sailed long ago...

Just for reference, I recently implemented this feature in pyright in response to user requests, and pyright users have been happy with the functionality.

My implementation uses the following rules. If these conditions are met, an error is generated for the child class declaration.
1. A class is not a protocol class but explicitly derives from a base class that is a protocol class. 
2. The base class is not declared in a stub file
3. The base class contains a method whose implementation consists only of docstrings and/or `...`

It also checks for the case where the base class contains an instance variable or class variable declaration without a value assignment and the child class also assigns no value for that variable.

FWIW, pyright doesn't treat a `pass` statement as an unimplemented method. It also doesn't ignore methods with `@overload` signatures or methods with `None` or `Any` return type annotations.

![Screen Shot 2022-02-04 at 9 11 09 AM](https://user-images.githubusercontent.com/7040122/152572695-76627dc2-ec60-446d-9666-586c27dc736f.png)

This has been implemented in pyright for about a month now, and we haven't received any negative feedback so far. I think that's evidence that this check will not break users. 

It would be atypical to subclass from a protocol and leave some members unimplemented. In the rare case where that's desired and intended, it would be appropriate to mark the subclass as a protocol also, simply by including `Protocol` in its list of base classes. This will eliminate the error condition and prevent the subclass from being instantiated without the type checker emitting an error.
Ehhhh. I'm not sure about the trivial body being implicitly abstract part. Protocol being an ABCMeta and `@abstractmethod` being used throughout the PEP imply that was the intended mechanism to mark non-implemented Protocol methods. I also don't like the fact that it doesn't apply to ABCs as well. Protocols and ABCs are essentially the same thing during defintion, so making them different makes me 😬. I think that's a slightly different concern than the OP.

If you haven't seen anyone break because of the change to check Protocol immediate subclasses, it's a good idea in my book. I might extend the functionality a bit:

> When inheriting from Protocols, if the subclass is intended to be a Protocol, it must be explicitly marked as such. If the subclass is intended to be an ABC, it must be explicitly marked as such. Otherwise it's treated as a concrete type. Concrete subclasses of Protocols must implement all abstract members. 

I'd really like to see the last sentence applied to subclasses of ABCs as well;  "Concrete subclasses or ABCs must implement all abstract members". This would make the rules between the two universal: Protocol and ABC being two different ways of saying abstract, and all other classes being concrete. I really don't like implicitly abstract classes. I want to be able to look at the class definition and see immediately if it's abstract or not.

If we can't do both, I'm less sure about it being a good idea. One should be like the other. Since `@abstractmethod` and `ABC` currently allow the definition of implicitly abstract classes at both runtime and in mypy, Protocol should behave the same way. But I guess that isn't my call.

Patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -67,7 +67,9 @@
     CONTRAVARIANT,
     COVARIANT,
     GDEF,
+    IMPLICITLY_ABSTRACT,
     INVARIANT,
+    IS_ABSTRACT,
     LDEF,
     LITERAL_TYPE,
     MDEF,
@@ -115,7 +117,6 @@
     ReturnStmt,
     StarExpr,
     Statement,
-    StrExpr,
     SymbolTable,
     SymbolTableNode,
     TempNode,
@@ -134,7 +135,7 @@
 from mypy.plugin import CheckerPluginInterface, Plugin
 from mypy.sametypes import is_same_type
 from mypy.scope import Scope
-from mypy.semanal import refers_to_fullname, set_callable_name
+from mypy.semanal import is_trivial_body, refers_to_fullname, set_callable_name
 from mypy.semanal_enum import ENUM_BASES, ENUM_SPECIAL_PROPS
 from mypy.sharedparse import BINARY_MAGIC_METHODS
 from mypy.state import state
@@ -618,7 +619,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
         for fdef in defn.items:
             assert isinstance(fdef, Decorator)
             self.check_func_item(fdef.func, name=fdef.func.name)
-            if fdef.func.is_abstract:
+            if fdef.func.abstract_status in (IS_ABSTRACT, IMPLICITLY_ABSTRACT):
                 num_abstract += 1
         if num_abstract not in (0, len(defn.items)):
             self.fail(message_registry.INCONSISTENT_ABSTRACT_OVERLOAD, defn)
@@ -1171,7 +1172,7 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
                     item.arguments[i].variable.type = arg_type
 
                 # Type check initialization expressions.
-                body_is_trivial = self.is_trivial_body(defn.body)
+                body_is_trivial = is_trivial_body(defn.body)
                 self.check_default_args(item, body_is_trivial)
 
             # Type check body in a new scope.
@@ -1339,49 +1340,6 @@ def check___new___signature(self, fdef: FuncDef, typ: CallableType) -> None:
                 "but must return a subtype of",
             )
 
-    def is_trivial_body(self, block: Block) -> bool:
-        """Returns 'true' if the given body is "trivial" -- if it contains just a "pass",
-        "..." (ellipsis), or "raise NotImplementedError()". A trivial body may also
-        start with a statement containing just a string (e.g. a docstring).
-
-        Note: functions that raise other kinds of exceptions do not count as
-        "trivial". We use this function to help us determine when it's ok to
-        relax certain checks on body, but functions that raise arbitrary exceptions
-        are more likely to do non-trivial work. For example:
-
-           def halt(self, reason: str = ...) -> NoReturn:
-               raise MyCustomError("Fatal error: " + reason, self.line, self.context)
-
-        A function that raises just NotImplementedError is much less likely to be
-        this complex.
-        """
-        body = block.body
-
-        # Skip a docstring
-        if body and isinstance(body[0], ExpressionStmt) and isinstance(body[0].expr, StrExpr):
-            body = block.body[1:]
-
-        if len(body) == 0:
-            # There's only a docstring (or no body at all).
-            return True
-        elif len(body) > 1:
-            return False
-
-        stmt = body[0]
-
-        if isinstance(stmt, RaiseStmt):
-            expr = stmt.expr
-            if expr is None:
-                return False
-            if isinstance(expr, CallExpr):
-                expr = expr.callee
-
-            return isinstance(expr, NameExpr) and expr.fullname == "builtins.NotImplementedError"
-
-        return isinstance(stmt, PassStmt) or (
-            isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, EllipsisExpr)
-        )
-
     def check_reverse_op_method(
         self, defn: FuncItem, reverse_type: CallableType, reverse_name: str, context: Context
     ) -> None:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -27,6 +27,7 @@
     ARG_POS,
     ARG_STAR,
     ARG_STAR2,
+    IMPLICITLY_ABSTRACT,
     LITERAL_TYPE,
     REVEAL_TYPE,
     ArgKind,
@@ -96,6 +97,7 @@
 )
 from mypy.sametypes import is_same_type
 from mypy.semanal_enum import ENUM_BASES
+from mypy.state import state
 from mypy.subtypes import is_equivalent, is_subtype, non_method_protocol_members
 from mypy.traverser import has_await_expression
 from mypy.typeanal import (
@@ -1236,24 +1238,32 @@ def check_callable_call(
 
         if (
             callee.is_type_obj()
-            and callee.type_object().is_abstract
+            and callee.type_object().is_protocol
             # Exception for Type[...]
             and not callee.from_type_type
-            and not callee.type_object().fallback_to_any
         ):
-            type = callee.type_object()
-            self.msg.cannot_instantiate_abstract_class(
-                callee.type_object().name, type.abstract_attributes, context
+            self.chk.fail(
+                message_registry.CANNOT_INSTANTIATE_PROTOCOL.format(callee.type_object().name),
+                context,
             )
         elif (
             callee.is_type_obj()
-            and callee.type_object().is_protocol
+            and callee.type_object().is_abstract
             # Exception for Type[...]
             and not callee.from_type_type
+            and not callee.type_object().fallback_to_any
         ):
-            self.chk.fail(
-                message_registry.CANNOT_INSTANTIATE_PROTOCOL.format(callee.type_object().name),
-                context,
+            type = callee.type_object()
+            # Determine whether the implicitly abstract attributes are functions with
+            # None-compatible return types.
+            abstract_attributes: Dict[str, bool] = {}
+            for attr_name, abstract_status in type.abstract_attributes:
+                if abstract_status == IMPLICITLY_ABSTRACT:
+                    abstract_attributes[attr_name] = self.can_return_none(type, attr_name)
+                else:
+                    abstract_attributes[attr_name] = False
+            self.msg.cannot_instantiate_abstract_class(
+                callee.type_object().name, abstract_attributes, context
             )
 
         formal_to_actual = map_actuals_to_formals(
@@ -1335,6 +1345,30 @@ def check_callable_call(
             callee = callee.copy_modified(ret_type=new_ret_type)
         return callee.ret_type, callee
 
+    def can_return_none(self, type: TypeInfo, attr_name: str) -> bool:
+        """Is the given attribute a method with a None-compatible return type?
+
+        Overloads are only checked if there is an implementation.
+        """
+        if not state.strict_optional:
+            # If strict-optional is not set, is_subtype(NoneType(), T) is always True.
+            # So, we cannot do anything useful here in that case.
+            return False
+        for base in type.mro:
+            symnode = base.names.get(attr_name)
+            if symnode is None:
+                continue
+            node = symnode.node
+            if isinstance(node, OverloadedFuncDef):
+                node = node.impl
+            if isinstance(node, Decorator):
+                node = node.func
+            if isinstance(node, FuncDef):
+                if node.type is not None:
+                    assert isinstance(node.type, CallableType)
+                    return is_subtype(NoneType(), node.type.ret_type)
+        return False
+
     def analyze_type_type_callee(self, item: ProperType, context: Context) -> Type:
         """Analyze the callee X in X(...) where X is Type[item].
 
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -1321,7 +1321,7 @@ def incompatible_conditional_function_def(self, defn: FuncDef) -> None:
         self.fail("All conditional function variants must have identical " "signatures", defn)
 
     def cannot_instantiate_abstract_class(
-        self, class_name: str, abstract_attributes: List[str], context: Context
+        self, class_name: str, abstract_attributes: Dict[str, bool], context: Context
     ) -> None:
         attrs = format_string_list([f'"{a}"' for a in abstract_attributes])
         self.fail(
@@ -1330,6 +1330,24 @@ def cannot_instantiate_abstract_class(
             context,
             code=codes.ABSTRACT,
         )
+        attrs_with_none = [
+            f'"{a}"'
+            for a, implicit_and_can_return_none in abstract_attributes.items()
+            if implicit_and_can_return_none
+        ]
+        if not attrs_with_none:
+            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."
+            )
+        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."
+            )
+        self.note(note.format(format_string_list(attrs_with_none)), context, code=codes.ABSTRACT)
 
     def base_class_definitions_incompatible(
         self, name: str, base1: TypeInfo, base2: TypeInfo, context: Context
diff --git a/mypy/nodes.py b/mypy/nodes.py
--- a/mypy/nodes.py
+++ b/mypy/nodes.py
@@ -758,7 +758,14 @@ def is_dynamic(self) -> bool:
         return self.type is None
 
 
-FUNCDEF_FLAGS: Final = FUNCITEM_FLAGS + ["is_decorated", "is_conditional", "is_abstract"]
+FUNCDEF_FLAGS: Final = FUNCITEM_FLAGS + ["is_decorated", "is_conditional"]
+
+# Abstract status of a function
+NOT_ABSTRACT: Final = 0
+# Explicitly abstract (with @abstractmethod or overload without implementation)
+IS_ABSTRACT: Final = 1
+# Implicitly abstract: used for functions with trivial bodies defined in Protocols
+IMPLICITLY_ABSTRACT: Final = 2
 
 
 class FuncDef(FuncItem, SymbolNode, Statement):
@@ -771,7 +778,7 @@ class FuncDef(FuncItem, SymbolNode, Statement):
         "_name",
         "is_decorated",
         "is_conditional",
-        "is_abstract",
+        "abstract_status",
         "original_def",
         "deco_line",
     )
@@ -788,7 +795,7 @@ def __init__(
         self._name = name
         self.is_decorated = False
         self.is_conditional = False  # Defined conditionally (within block)?
-        self.is_abstract = False
+        self.abstract_status = NOT_ABSTRACT
         self.is_final = False
         # Original conditional definition
         self.original_def: Union[None, FuncDef, Var, Decorator] = None
@@ -817,6 +824,7 @@ def serialize(self) -> JsonDict:
             "arg_kinds": [int(x.value) for x in self.arg_kinds],
             "type": None if self.type is None else self.type.serialize(),
             "flags": get_flags(self, FUNCDEF_FLAGS),
+            "abstract_status": self.abstract_status,
             # TODO: Do we need expanded, original_def?
         }
 
@@ -839,6 +847,7 @@ def deserialize(cls, data: JsonDict) -> "FuncDef":
         # NOTE: ret.info is set in the fixup phase.
         ret.arg_names = data["arg_names"]
         ret.arg_kinds = [ArgKind(x) for x in data["arg_kinds"]]
+        ret.abstract_status = data["abstract_status"]
         # Leave these uninitialized so that future uses will trigger an error
         del ret.arguments
         del ret.max_pos
@@ -2674,7 +2683,9 @@ class is generic then it will be a type constructor of higher kind.
     is_abstract: bool  # Does the class have any abstract attributes?
     is_protocol: bool  # Is this a protocol class?
     runtime_protocol: bool  # Does this protocol support isinstance checks?
-    abstract_attributes: List[str]
+    # List of names of abstract attributes together with their abstract status.
+    # The abstract status must be one of `NOT_ABSTRACT`, `IS_ABSTRACT`, `IMPLICITLY_ABSTRACT`.
+    abstract_attributes: List[Tuple[str, int]]
     deletable_attributes: List[str]  # Used by mypyc only
     # Does this type have concrete `__slots__` defined?
     # If class does not have `__slots__` defined then it is `None`,
@@ -3034,7 +3045,7 @@ def deserialize(cls, data: JsonDict) -> "TypeInfo":
         ti = TypeInfo(names, defn, module_name)
         ti._fullname = data["fullname"]
         # TODO: Is there a reason to reconstruct ti.subtypes?
-        ti.abstract_attributes = data["abstract_attributes"]
+        ti.abstract_attributes = [(attr[0], attr[1]) for attr in data["abstract_attributes"]]
         ti.type_vars = data["type_vars"]
         ti.has_param_spec_type = data["has_param_spec_type"]
         ti.bases = [mypy.types.Instance.deserialize(b) for b in data["bases"]]
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -84,7 +84,9 @@
     CONTRAVARIANT,
     COVARIANT,
     GDEF,
+    IMPLICITLY_ABSTRACT,
     INVARIANT,
+    IS_ABSTRACT,
     LDEF,
     MDEF,
     REVEAL_LOCALS,
@@ -145,6 +147,7 @@
     OverloadedFuncDef,
     OverloadPart,
     ParamSpecExpr,
+    PassStmt,
     PlaceholderNode,
     PromoteExpr,
     RaiseStmt,
@@ -837,6 +840,20 @@ def analyze_func_def(self, defn: FuncDef) -> None:
 
         self.analyze_arg_initializers(defn)
         self.analyze_function_body(defn)
+
+        if self.is_class_scope():
+            assert self.type is not None
+            # Mark protocol methods with empty bodies as implicitly abstract.
+            # This makes explicit protocol subclassing type-safe.
+            if (
+                self.type.is_protocol
+                and not self.is_stub_file  # Bodies in stub files are always empty.
+                and (not isinstance(self.scope.function, OverloadedFuncDef) or defn.is_property)
+                and defn.abstract_status != IS_ABSTRACT
+                and is_trivial_body(defn.body)
+            ):
+                defn.abstract_status = IMPLICITLY_ABSTRACT
+
         if (
             defn.is_coroutine
             and isinstance(defn.type, CallableType)
@@ -975,6 +992,21 @@ def analyze_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
         # We know this is an overload def. Infer properties and perform some checks.
         self.process_final_in_overload(defn)
         self.process_static_or_class_method_in_overload(defn)
+        self.process_overload_impl(defn)
+
+    def process_overload_impl(self, defn: OverloadedFuncDef) -> None:
+        """Set flags for an overload implementation.
+
+        Currently, this checks for a trivial body in protocols classes,
+        where it makes the method implicitly abstract.
+        """
+        if defn.impl is None:
+            return
+        impl = defn.impl if isinstance(defn.impl, FuncDef) else defn.impl.func
+        if is_trivial_body(impl.body) and self.is_class_scope() and not self.is_stub_file:
+            assert self.type is not None
+            if self.type.is_protocol:
+                impl.abstract_status = IMPLICITLY_ABSTRACT
 
     def analyze_overload_sigs_and_impl(
         self, defn: OverloadedFuncDef
@@ -1052,12 +1084,13 @@ def handle_missing_overload_implementation(self, defn: OverloadedFuncDef) -> Non
         """Generate error about missing overload implementation (only if needed)."""
         if not self.is_stub_file:
             if self.type and self.type.is_protocol and not self.is_func_scope():
-                # An overloaded protocol method doesn't need an implementation.
+                # An overloaded protocol method doesn't need an implementation,
+                # but if it doesn't have one, then it is considered abstract.
                 for item in defn.items:
                     if isinstance(item, Decorator):
-                        item.func.is_abstract = True
+                        item.func.abstract_status = IS_ABSTRACT
                     else:
-                        item.is_abstract = True
+                        item.abstract_status = IS_ABSTRACT
             else:
                 self.fail(
                     "An overloaded function outside a stub file must have an implementation",
@@ -1133,7 +1166,7 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) -
                             # The first item represents the entire property.
                             first_item.var.is_settable_property = True
                             # Get abstractness from the original definition.
-                            item.func.is_abstract = first_item.func.is_abstract
+                            item.func.abstract_status = first_item.func.abstract_status
                 else:
                     self.fail("Decorated property not supported", item)
                 item.func.accept(self)
@@ -1220,7 +1253,7 @@ def visit_decorator(self, dec: Decorator) -> None:
             # A bunch of decorators are special cased here.
             if refers_to_fullname(d, "abc.abstractmethod"):
                 removed.append(i)
-                dec.func.is_abstract = True
+                dec.func.abstract_status = IS_ABSTRACT
                 self.check_decorated_function_is_method("abstractmethod", dec)
             elif refers_to_fullname(d, ("asyncio.coroutines.coroutine", "types.coroutine")):
                 removed.append(i)
@@ -1242,7 +1275,7 @@ def visit_decorator(self, dec: Decorator) -> None:
                 dec.func.is_property = True
                 dec.var.is_property = True
                 if refers_to_fullname(d, "abc.abstractproperty"):
-                    dec.func.is_abstract = True
+                    dec.func.abstract_status = IS_ABSTRACT
                 elif refers_to_fullname(d, "functools.cached_property"):
                     dec.var.is_settable_property = True
                 self.check_decorated_function_is_method("property", dec)
@@ -1271,7 +1304,7 @@ def visit_decorator(self, dec: Decorator) -> None:
             dec.func.accept(self)
         if dec.decorators and dec.var.is_property:
             self.fail("Decorated property not supported", dec)
-        if dec.func.is_abstract and dec.func.is_final:
+        if dec.func.abstract_status == IS_ABSTRACT and dec.func.is_final:
             self.fail(f"Method {dec.func.name} is both abstract and final", dec)
 
     def check_decorated_function_is_method(self, decorator: str, context: Context) -> None:
@@ -6033,3 +6066,47 @@ def is_same_symbol(a: Optional[SymbolNode], b: Optional[SymbolNode]) -> bool:
         or (isinstance(a, PlaceholderNode) and isinstance(b, PlaceholderNode))
         or is_same_var_from_getattr(a, b)
     )
+
+
+def is_trivial_body(block: Block) -> bool:
+    """Returns 'true' if the given body is "trivial" -- if it contains just a "pass",
+    "..." (ellipsis), or "raise NotImplementedError()". A trivial body may also
+    start with a statement containing just a string (e.g. a docstring).
+
+    Note: functions that raise other kinds of exceptions do not count as
+    "trivial". We use this function to help us determine when it's ok to
+    relax certain checks on body, but functions that raise arbitrary exceptions
+    are more likely to do non-trivial work. For example:
+
+       def halt(self, reason: str = ...) -> NoReturn:
+           raise MyCustomError("Fatal error: " + reason, self.line, self.context)
+
+    A function that raises just NotImplementedError is much less likely to be
+    this complex.
+    """
+    body = block.body
+
+    # Skip a docstring
+    if body and isinstance(body[0], ExpressionStmt) and isinstance(body[0].expr, StrExpr):
+        body = block.body[1:]
+
+    if len(body) == 0:
+        # There's only a docstring (or no body at all).
+        return True
+    elif len(body) > 1:
+        return False
+
+    stmt = body[0]
+
+    if isinstance(stmt, RaiseStmt):
+        expr = stmt.expr
+        if expr is None:
+            return False
+        if isinstance(expr, CallExpr):
+            expr = expr.callee
+
+        return isinstance(expr, NameExpr) and expr.fullname == "builtins.NotImplementedError"
+
+    return isinstance(stmt, PassStmt) or (
+        isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, EllipsisExpr)
+    )
diff --git a/mypy/semanal_classprop.py b/mypy/semanal_classprop.py
--- a/mypy/semanal_classprop.py
+++ b/mypy/semanal_classprop.py
@@ -3,14 +3,17 @@
 These happen after semantic analysis and before type checking.
 """
 
-from typing import List, Optional, Set
+from typing import List, Optional, Set, Tuple
 
 from typing_extensions import Final
 
 from mypy.errors import Errors
 from mypy.nodes import (
+    IMPLICITLY_ABSTRACT,
+    IS_ABSTRACT,
     CallExpr,
     Decorator,
+    FuncDef,
     Node,
     OverloadedFuncDef,
     PromoteExpr,
@@ -46,7 +49,8 @@ def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: E
     if typ.typeddict_type:
         return  # TypedDict can't be abstract
     concrete: Set[str] = set()
-    abstract: List[str] = []
+    # List of abstract attributes together with their abstract status
+    abstract: List[Tuple[str, int]] = []
     abstract_in_this_class: List[str] = []
     if typ.is_newtype:
         # Special case: NewTypes are considered as always non-abstract, so they can be used as:
@@ -69,16 +73,20 @@ def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: E
             else:
                 func = node
             if isinstance(func, Decorator):
-                fdef = func.func
-                if fdef.is_abstract and name not in concrete:
+                func = func.func
+            if isinstance(func, FuncDef):
+                if (
+                    func.abstract_status in (IS_ABSTRACT, IMPLICITLY_ABSTRACT)
+                    and name not in concrete
+                ):
                     typ.is_abstract = True
-                    abstract.append(name)
+                    abstract.append((name, func.abstract_status))
                     if base is typ:
                         abstract_in_this_class.append(name)
             elif isinstance(node, Var):
                 if node.is_abstract_var and name not in concrete:
                     typ.is_abstract = True
-                    abstract.append(name)
+                    abstract.append((name, IS_ABSTRACT))
                     if base is typ:
                         abstract_in_this_class.append(name)
             concrete.add(name)
@@ -96,13 +104,13 @@ def calculate_class_abstract_status(typ: TypeInfo, is_stub_file: bool, errors: E
             def report(message: str, severity: str) -> None:
                 errors.report(typ.line, typ.column, message, severity=severity)
 
-            attrs = ", ".join(f'"{attr}"' for attr in sorted(abstract))
+            attrs = ", ".join(f'"{attr}"' for attr, _ in sorted(abstract))
             report(f"Class {typ.fullname} has abstract attributes {attrs}", "error")
             report(
                 "If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass", "note"
             )
     if typ.is_final and abstract:
-        attrs = ", ".join(f'"{attr}"' for attr in sorted(abstract))
+        attrs = ", ".join(f'"{attr}"' for attr, _ in sorted(abstract))
         errors.report(
             typ.line, typ.column, f"Final class {typ.fullname} has abstract attributes {attrs}"
         )
diff --git a/mypy/strconv.py b/mypy/strconv.py
--- a/mypy/strconv.py
+++ b/mypy/strconv.py
@@ -133,7 +133,7 @@ def visit_func_def(self, o: "mypy.nodes.FuncDef") -> str:
         arg_kinds = {arg.kind for arg in o.arguments}
         if len(arg_kinds & {mypy.nodes.ARG_NAMED, mypy.nodes.ARG_NAMED_OPT}) > 0:
             a.insert(1, f"MaxPos({o.max_pos})")
-        if o.is_abstract:
+        if o.abstract_status in (mypy.nodes.IS_ABSTRACT, mypy.nodes.IMPLICITLY_ABSTRACT):
             a.insert(-1, "Abstract")
         if o.is_static:
             a.insert(-1, "Static")
diff --git a/mypy/stubgen.py b/mypy/stubgen.py
--- a/mypy/stubgen.py
+++ b/mypy/stubgen.py
@@ -71,6 +71,7 @@
     ARG_POS,
     ARG_STAR,
     ARG_STAR2,
+    IS_ABSTRACT,
     AssignmentStmt,
     Block,
     BytesExpr,
@@ -723,7 +724,9 @@ def visit_func_def(
                 retname = None  # implicit Any
             else:
                 retname = self.print_annotation(o.unanalyzed_type.ret_type)
-        elif isinstance(o, FuncDef) and (o.is_abstract or o.name in METHODS_WITH_RETURN_VALUE):
+        elif isinstance(o, FuncDef) and (
+            o.abstract_status == IS_ABSTRACT or o.name in METHODS_WITH_RETURN_VALUE
+        ):
             # Always assume abstract methods return Any unless explicitly annotated. Also
             # some dunder methods should not have a None return type.
             retname = None  # implicit Any
@@ -910,16 +913,16 @@ def visit_class_def(self, o: ClassDef) -> None:
         if isinstance(o.metaclass, (NameExpr, MemberExpr)):
             meta = o.metaclass.accept(AliasPrinter(self))
             base_types.append("metaclass=" + meta)
-        elif self.analyzed and o.info.is_abstract:
-            base_types.append("metaclass=abc.ABCMeta")
-            self.import_tracker.add_import("abc")
-            self.import_tracker.require_name("abc")
         elif self.analyzed and o.info.is_protocol:
             type_str = "Protocol"
             if o.info.type_vars:
                 type_str += f'[{", ".join(o.info.type_vars)}]'
             base_types.append(type_str)
             self.add_typing_import("Protocol")
+        elif self.analyzed and o.info.is_abstract:
+            base_types.append("metaclass=abc.ABCMeta")
+            self.import_tracker.add_import("abc")
+            self.import_tracker.require_name("abc")
         if base_types:
             self.add(f"({', '.join(base_types)})")
         self.add(":\n")
diff --git a/mypy/treetransform.py b/mypy/treetransform.py
--- a/mypy/treetransform.py
+++ b/mypy/treetransform.py
@@ -186,7 +186,7 @@ def visit_func_def(self, node: FuncDef) -> FuncDef:
         new._fullname = node._fullname
         new.is_decorated = node.is_decorated
         new.is_conditional = node.is_conditional
-        new.is_abstract = node.is_abstract
+        new.abstract_status = node.abstract_status
         new.is_static = node.is_static
         new.is_class = node.is_class
         new.is_property = node.is_property


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
@@ -444,7 +444,7 @@ class P(C, Protocol): # E: All bases of a protocol must be protocols
 class P2(P, D, Protocol): # E: All bases of a protocol must be protocols
     pass
 
-P2() # E: Cannot instantiate abstract class "P2" with abstract attribute "attr"
+P2() # E: Cannot instantiate protocol class "P2"
 p: P2
 reveal_type(p.attr) # N: Revealed type is "builtins.int"
 
@@ -1553,7 +1553,7 @@ f2(z) # E: Argument 1 to "f2" has incompatible type "Union[C, D1]"; expected "P2
 from typing import Type, Protocol
 
 class P(Protocol):
-    def m(self) -> None: pass
+    def m(self) -> None: return None
 class P1(Protocol):
     def m(self) -> None: pass
 class Pbad(Protocol):
@@ -1599,7 +1599,7 @@ f(GoodAlias)
 from typing import Type, Protocol
 
 class P(Protocol):
-    def m(self) -> None: pass
+    def m(self) -> None: return None
 class B(P): pass
 class C:
     def m(self) -> None:
@@ -2924,3 +2924,196 @@ class C:
 def round(number: SupportsRound[_T], ndigits: int) -> _T: ...
 
 round(C(), 1)
+
+[case testEmptyBodyImplicitlyAbstractProtocol]
+# flags: --strict-optional
+from typing import Protocol, overload, Union
+
+class P1(Protocol):
+    def meth(self) -> int: ...
+class B1(P1): ...
+class C1(P1):
+    def meth(self) -> int:
+        return 0
+B1()  # E: Cannot instantiate abstract class "B1" with abstract attribute "meth"
+C1()
+
+class P2(Protocol):
+    @classmethod
+    def meth(cls) -> int: ...
+class B2(P2): ...
+class C2(P2):
+    @classmethod
+    def meth(cls) -> int:
+        return 0
+B2()  # E: Cannot instantiate abstract class "B2" with abstract attribute "meth"
+C2()
+
+class P3(Protocol):
+    @overload
+    def meth(self, x: int) -> int: ...
+    @overload
+    def meth(self, x: str) -> str: ...
+    @overload
+    def not_abstract(self, x: int) -> int: ...
+    @overload
+    def not_abstract(self, x: str) -> str: ...
+    def not_abstract(self, x: Union[int, str]) -> Union[int, str]:
+        return 0
+class B3(P3): ...
+class C3(P3):
+    @overload
+    def meth(self, x: int) -> int: ...
+    @overload
+    def meth(self, x: str) -> str: ...
+    def meth(self, x: Union[int, str]) -> Union[int, str]:
+        return 0
+B3()  # E: Cannot instantiate abstract class "B3" with abstract attribute "meth"
+C3()
+[builtins fixtures/classmethod.pyi]
+
+[case testEmptyBodyImplicitlyAbstractProtocolProperty]
+# flags: --strict-optional
+from typing import Protocol
+
+class P1(Protocol):
+    @property
+    def attr(self) -> int: ...
+class B1(P1): ...
+class C1(P1):
+    @property
+    def attr(self) -> int:
+        return 0
+B1()  # E: Cannot instantiate abstract class "B1" with abstract attribute "attr"
+C1()
+
+class P2(Protocol):
+    @property
+    def attr(self) -> int: ...
+    @attr.setter
+    def attr(self, value: int) -> None: ...
+class B2(P2): ...
+class C2(P2):
+    @property
+    def attr(self) -> int: return 0
+    @attr.setter
+    def attr(self, value: int) -> None: pass
+B2()  # E: Cannot instantiate abstract class "B2" with abstract attribute "attr"
+C2()
+[builtins fixtures/property.pyi]
+
+[case testEmptyBodyImplicitlyAbstractProtocolStub]
+from stub import P1, P2, P3, P4
+
+class B1(P1): ...
+class B2(P2): ...
+class B3(P3): ...
+class B4(P4): ...
+
+B1()
+B2()
+B3()
+B4()  # E: Cannot instantiate abstract class "B4" with abstract attribute "meth"
+
+[file stub.pyi]
+from typing import Protocol, overload, Union
+from abc import abstractmethod
+
+class P1(Protocol):
+    def meth(self) -> int: ...
+
+class P2(Protocol):
+    @classmethod
+    def meth(cls) -> int: ...
+
+class P3(Protocol):
+    @overload
+    def meth(self, x: int) -> int: ...
+    @overload
+    def meth(self, x: str) -> str: ...
+
+class P4(Protocol):
+    @abstractmethod
+    def meth(self) -> int: ...
+[builtins fixtures/classmethod.pyi]
+
+[case testEmptyBodyVariationsImplicitlyAbstractProtocol]
+from typing import Protocol
+
+class WithPass(Protocol):
+    def meth(self) -> int:
+        pass
+class A(WithPass): ...
+A() # E: Cannot instantiate abstract class "A" with abstract attribute "meth"
+
+class WithEllipses(Protocol):
+    def meth(self) -> int: ...
+class B(WithEllipses): ...
+B() # E: Cannot instantiate abstract class "B" with abstract attribute "meth"
+
+class WithDocstring(Protocol):
+    def meth(self) -> int:
+        """Docstring for meth.
+
+	This is meth."""
+class C(WithDocstring): ...
+C() # E: Cannot instantiate abstract class "C" with abstract attribute "meth"
+
+class WithRaise(Protocol):
+    def meth(self) -> int:
+        """Docstring for meth."""
+        raise NotImplementedError
+class D(WithRaise): ...
+D() # E: Cannot instantiate abstract class "D" with abstract attribute "meth"
+[builtins fixtures/exception.pyi]
+
+[case testEmptyBodyNoneCompatibleProtocol]
+# flags: --strict-optional
+from abc import abstractmethod
+from typing import Any, Optional, Protocol, Union, overload
+from typing_extensions import TypeAlias
+
+NoneAlias: TypeAlias = None
+
+class NoneCompatible(Protocol):
+    def f(self) -> None: ...
+    def g(self) -> Any: ...
+    def h(self) -> Optional[int]: ...
+    def i(self) -> NoneAlias: ...
+    @classmethod
+    def j(cls) -> None: ...
+
+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.
+
+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.
+
+class NoneCompatible3(Protocol):
+    @abstractmethod
+    def f(self) -> None: ...
+    @overload
+    def g(self, x: int) -> int: ...
+    @overload
+    def g(self, x: str) -> None: ...
+    def h(self, x): ...
+
+class C(NoneCompatible3): ...
+C() # E: Cannot instantiate abstract class "C" with abstract attributes "f", "g" and "h"
+[builtins fixtures/tuple.pyi]
+[builtins fixtures/classmethod.pyi]
+
+[case testEmptyBodyWithFinal]
+from typing import Protocol, final
+
+class P(Protocol):
+    @final          # E: Protocol member cannot be final
+    def f(self, x: int) -> str: ...
+
+class A(P): ...
+A() # E: Cannot instantiate abstract class "A" with abstract attribute "f"
diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test
--- a/test-data/unit/stubgen.test
+++ b/test-data/unit/stubgen.test
@@ -2675,6 +2675,23 @@ T2 = TypeVar('T2')
 class PT(Protocol[T, T2]):
     def f(self, x: T) -> T2: ...
 
+[case testProtocolAbstractMethod_semanal]
+from abc import abstractmethod
+from typing import Protocol
+
+class P(Protocol):
+    @abstractmethod
+    def f(self, x: int, y: int) -> str:
+        ...
+
+[out]
+from abc import abstractmethod
+from typing import Protocol
+
+class P(Protocol):
+    @abstractmethod
+    def f(self, x: int, y: int) -> str: ...
+
 [case testNonDefaultKeywordOnlyArgAfterAsterisk]
 def func(*, non_default_kwarg: bool, default_kwarg: bool = True): ...
 [out]