Skip to content

Commit 7ed91bc

Browse files
srittauAkuli
andauthored
Add _typeshed.MaybeNone as Any trick marker (#11815)
Co-authored-by: Akuli <[email protected]>
1 parent ed7f35a commit 7ed91bc

File tree

2 files changed

+22
-44
lines changed

2 files changed

+22
-44
lines changed

CONTRIBUTING.md

Lines changed: 15 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -554,58 +554,31 @@ It should be used sparingly.
554554

555555
### "The `Any` trick"
556556

557+
In cases where a function or method can return `None`, but where forcing the
558+
user to explicitly check for `None` can be detrimental, use
559+
`_typeshed.MaybeNone` (an alias to `Any`), instead of `None`.
560+
557561
Consider the following (simplified) signature of `re.Match[str].group`:
558562

559563
```python
560564
class Match:
561-
def group(self, group: str | int, /) -> str | Any: ...
565+
def group(self, group: str | int, /) -> str | MaybeNone: ...
562566
```
563567

564-
The `str | Any` seems unnecessary and weird at first.
565-
Because `Any` includes all strings, you would expect `str | Any` to be
566-
equivalent to `Any`, but it is not. To understand the difference,
567-
let's look at what happens when type-checking this simplified example:
568-
569-
Suppose you have a legacy system that for historical reasons has two kinds
570-
of user IDs. Old IDs look like `"legacy_userid_123"` and new IDs look like
571-
`"456_username"`. The function below is supposed to extract the name
572-
`"USERNAME"` from a new ID, and return `None` if you give it a legacy ID.
568+
This avoid forcing the user to check for `None`:
573569

574570
```python
575-
import re
576-
577-
def parse_name_from_new_id(user_id: str) -> str | None:
578-
match = re.fullmatch(r"\d+_(.*)", user_id)
579-
if match is None:
580-
return None
581-
name_group = match.group(1)
582-
return name_group.uper() # This line is a typo (`uper` --> `upper`)
571+
match = re.fullmatch(r"\d+_(.*)", some_string)
572+
assert match is not None
573+
name_group = match.group(1) # The user knows that this will never be None
574+
return name_group.uper() # This typo will be flagged by the type checker
583575
```
584576

585-
The `.group()` method returns `None` when the given group was not a part of the match.
586-
For example, with a regex like `r"\d+_(.*)|legacy_userid_\d+"`, we would get a match whose `.group(1)` is `None` for the user ID `"legacy_userid_7"`.
587-
But here the regex is written so that the group always exists, and `match.group(1)` cannot return `None`.
588-
Match groups are almost always used in this way.
589-
590-
Let's now consider typeshed's `-> str | Any` annotation of the `.group()` method:
591-
592-
* `-> Any` would mean "please do not complain" to type checkers.
593-
If `name_group` has type `Any`, you will get no error for this.
594-
* `-> str` would mean "will always be a `str`", which is wrong, and would
595-
cause type checkers to emit errors for code like `if name_group is None`.
596-
* `-> str | None` means "you must check for None", which is correct but can get
597-
annoying for some common patterns. Checks like `assert name_group is not None`
598-
would need to be added into various places only to satisfy type checkers,
599-
even when it is impossible to actually get a `None` value
600-
(type checkers aren't smart enough to know this).
601-
* `-> str | Any` means "must be prepared to handle a `str`". You will get an
602-
error for `name_group.uper`, because it is not valid when `name_group` is a
603-
`str`. But type checkers are happy with `if name_group is None` checks,
604-
because we're saying it can also be something else than an `str`.
605-
606-
In typeshed we unofficially call returning `Foo | Any` "the Any trick".
607-
We tend to use it whenever something can be `None`,
608-
but requiring users to check for `None` would be more painful than helpful.
577+
In this case, the user of `match.group()` must be prepared to handle a `str`,
578+
but type checkers are happy with `if name_group is None` checks, because we're
579+
saying it can also be something else than an `str`.
580+
581+
This is sometimes called "the Any trick".
609582

610583
## Submitting Changes
611584

stdlib/_typeshed/__init__.pyi

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,15 @@ AnyStr_co = TypeVar("AnyStr_co", str, bytes, covariant=True) # noqa: Y001
4747
# isn't possible or a type is already partially known. In cases like these,
4848
# use Incomplete instead of Any as a marker. For example, use
4949
# "Incomplete | None" instead of "Any | None".
50-
Incomplete: TypeAlias = Any
50+
Incomplete: TypeAlias = Any # stable
5151

5252
# To describe a function parameter that is unused and will work with anything.
53-
Unused: TypeAlias = object
53+
Unused: TypeAlias = object # stable
54+
55+
# Marker for return types that include None, but where forcing the user to
56+
# check for None can be detrimental. Sometimes called "the Any trick". See
57+
# CONTRIBUTING.md for more information.
58+
MaybeNone: TypeAlias = Any # stable
5459

5560
# Used to mark arguments that default to a sentinel value. This prevents
5661
# stubtest from complaining about the default value not matching.

0 commit comments

Comments
 (0)