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?
SomeType | Any
seems to imply that the return type could be anything, when what I want to say is that it can beSomeType
or in weird casesNone
, so this doesn't seem to express the intent.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?
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?
SomeType | Any
seems to imply that the return type could be anything, when what I want to say is that it can beSomeType
or in weird casesNone
, so this doesn't seem to express the intent.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?
2 Answers
Reset to default 6
SomeType | Any
seems to imply that the return type could be anything [...]
SomeType | Any
is not the same as Any
. It means "a type that is lowerbounded to SomeType
".
To give a simple example:
(playgrounds: Mypy, Pyright)
a: Any
a.foo() # no error
class C:
b: int
c: C | Any
c.foo() # error: `C` has no attribute `foo`
For an operation not to cause any type errors when applied to an union, it must be type-safe when applied to every member of that union. Since C | None
has None
as a member, many operations like attribute access (None.foo
), subscription (None[...]
) and invocation (None()
) would fail.
This is considered not user-friendly,[citation needed] especially when Python does not have a built-in "assert not None
" shorthand. Using MaybeNone
/Any
instead of None
saves the user from an unergonomic check without sacrificing (too much) type safety.
The explanation has since been removed from CONTRIBUTING.md
in a more recent PR (it predates the comment).
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, it was updated to def getroot(self) -> Element | Any: ...
with the possible return types + Any.
The different possibilities and effects are summarized as:
-> Any
means "please do not complain" to type checkers. Ifroot
has typeAny
, you will no error for this.
-> Element
means "will always be anElement
", which is wrong, and would cause type checkers to emit errors for code likeif 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 likeET.parse("file.xml").getroot().iter("whatever")
.
-> Element | Any
means "must be prepared to handle an Element". You will get an error forroot.tagg
, because it is not valid when root is an Element. But type checkers are happy withif 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