> so I'm pretty sure the new preferred way is to explicitly use abstract superclasses... just like Java did all along (and is mandatory).

typing.Protocol is a good fit for this use case

  from typing import Protocol
  
  class HasMessage(Protocol):
      def get_message(self) -> str: ...
  
  class A:
      """Implicit (duck-typed)"""
      def get_message(self) -> str:
          return "A"
  
  class B(HasMessage):
      """Explicit"""
      def get_message(self) -> str:
          return "B"
  
  class C:
      def get_message(self) -> int:
          return 1
  
  def print_message(m: HasMessage) -> None:
      print(m.get_message())
  
  print_message(A())
  print_message(B())
  print_message(C())  # fails type check

Are you ever supposed to inherit from the protocol though(unless you’re defining another protocol)? One of the great things about protocols is that your class doesn’t even have to know about the protocol explicitly. What this code looks closer to doing (style wise) is an abstract base class