pythonpython-typingtypeshed

How does "MaybeNone" (also known as "The Any Trick") work in Python type hints?


In typestubs for the Python standard library I noticed a peculiar type called MaybeNone pop up, usually in the form of NormalType | MaybeNone. For example, in the sqlite3-Cursor class I find this:

class Cursor:
    # May be None, but using `| MaybeNone` (`| Any`) instead to avoid slightly annoying false positives.
    @property
    def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...] | MaybeNone: ...

The definition of this MaybeNone is given as:

# Marker for return types that include None, but where forcing the user to
# check for None can be detrimental. Sometimes called "the Any trick". See
# CONTRIBUTING.md for more information.
MaybeNone: TypeAlias = Any  # stable

(I could not find additional information in the CONTRIBUTING.md, which I assume to be this one.)

I understand the intention of marking a return type in such a way that a user is not forced to null check in cases where the null is more of a theoretical problem for most users. But how does this achieve the goal?

  1. SomeType | Any seems to imply that the return type could be anything, when what I want to say is that it can be SomeType or in weird cases None, so this doesn't seem to express the intent.

  2. MyPy already allows superfluous null-checks on variables that can be proven not to be None even with --strict (at least with my configuration?) so what does the special typing even accomplish as compared to simply doing nothing?


Solution

  • A nice summary can be found in this comment explaining the "Any Trick" of typeshed.

    We tend to use it whenever something can be None, but requiring users to check for None would be more painful than helpful.

    As background they talk about xml.etree.ElementTree.getroot which in some case returns None (Happens when the tree is initialized without a root).
    To reflect this, getroot was updated to def getroot(self) -> Element | Any: ... with the possible return types(Element) and additionally | Any.

    The different possibilities and effects are summarized as:

    -> Any means "please do not complain" to type checkers. If root has type Any, you will no error for this.

    -> Element means "will always be an Element", which is wrong, and would cause type checkers to emit errors for code like if root is None.

    -> Element | None means "you must check for None", which is correct but can get annoying. [..., it could be possible used] to do things like ET.parse("file.xml").getroot().iter("whatever").

    -> Element | Any means "must be prepared to handle an Element". You will get an error for root.tagg, because it is not valid when root is an Element. But type checkers are happy with if root is None checks, because we're saying it can also be something else than an Element.

    I did slightly modify the quotes by adding italics and -> type