If you're using Python's static typing features—specifically typing.Protocol—alongside VS Code's Pylance or the Pyright type checker, you might have run into an annoying error. You define an interface method with an empty body containing pass, but Pyright flags it with a message like: "Function with declared return type 'str' must return value on all code paths."

Why does Pyright complain about this when the method is clearly part of an abstract protocol? Here is a breakdown of why this happens and how to resolve it cleanly.

The Problem Explained

Consider this standard protocol definition:

from typing import Optional, Protocol

class Talkable(Protocol):
    def talk(self, topic: Optional[str] = None) -> str:
        pass

At runtime, a Python function that hits a pass statement without an explicit return statement implicitly returns None. Because Pyright performs strict return-path analysis, it sees pass as concrete code executing a path that yields None instead of the expected str.

Even though the class inherits from Protocol, Python allows protocol methods to have default implementations. Because of this, Pyright cannot simply assume every method inside a Protocol is an empty interface stub if you use regular executable statements like pass.

The Solution: Use Ellipsis (...)

In Python type hints and stub files (governed by PEP 484 and PEP 544), the idiomatic way to define an abstract or un-implemented method body is using the ellipsis literal (...), rather than pass.

Pyright specifically recognizes the ellipsis as a stub marker and disables the return-path check for it:

from typing import Optional, Protocol

class Talkable(Protocol):
    def talk(self, topic: Optional[str] = None) -> str:
        ...

By replacing pass with ..., Pyright immediately understands that talk is a signature definition with no implementation, and the error disappears.

What If You Want Docstrings?

Documenting interface methods is common practice. If you add a docstring, you should still include the ellipsis right after it:

class Talkable(Protocol):
    def talk(self, topic: Optional[str] = None) -> str:
        """Generate a talk on the given topic."""
        ...

Note: In some type checkers, a docstring alone without an explicit body might also trigger return-type warnings. Adding ... below the docstring ensures full compatibility across Pyright, mypy, and other static analysis tools.

Alternative: Raising NotImplementedError

If you prefer an explicit runtime safeguard to prevent anyone from calling super().talk(), you can raise an exception:

class Talkable(Protocol):
    def talk(self, topic: Optional[str] = None) -> str:
        raise NotImplementedError

Pyright recognizes that raise terminates the execution path without returning, so it won't raise the missing return value warning here either. However, for structural subtyping (protocols), the ellipsis (...) is generally preferred because protocols are meant to be pure interfaces rather than base classes.

Summary

  • pass is interpreted as executable code that returns None, violating non-None return type annotations.
  • ... (Ellipsis) is the official, PEP-standardized marker for protocol and stub declarations.
  • Replace pass with ... across your Protocol and @abstractmethod definitions to keep Pyright and Pylance completely happy.