I have a function that looks roughly like this:
import datetime
from typing import Union
class Sentinel(object): pass
sentinel = Sentinel()
def func(
dt: datetime.datetime,
as_tz: Union[datetime.tzinfo, None, Sentinel] = sentinel,
) -> str:
if as_tz is not sentinel:
# Never reached if as_tz has wrong type (Sentinel)
dt = dt.astimezone(as_tz)
# ...
# do other meaningful stuff
# ...
return "foo"
The sentinel
value is used here because None
is already a valid argument to .astimezone()
, so the purpose is to correctly identify cases where the user doesn't want to call .astimezone()
at all.
However, mypy
complains about this pattern with:
error: Argument 1 to "astimezone" of "datetime" has incompatible type "Union[tzinfo, None, Sentinel]"; expected "Optional[tzinfo]"
It seems that's because the datetime
stub (rightfully so) uses:
def astimezone(self, tz: Optional[_tzinfo] = ...) -> datetime: ...
But, is there a way to let mypy know that the sentinel
value will never be passed to .astimezone()
because of the if
check? Or does this just need a # type: ignore
with there being no cleaner way?
Another exampe:
from typing import Optional
import requests
def func(session: Optional[requests.Session] = None):
new_session_made = session is None
if new_session_made:
session = requests.Session()
try:
session.request("GET", "https://a.b.c.d.com/foo")
# ...
finally:
if new_session_made:
session.close()
This second, like the first, is "runtime safe" (for lack of a better term): the AttributeError
from calling None.request()
and None.close()
will not be reached or evaluated. However, mypy still complains that:
mypytest.py:9: error: Item "None" of "Optional[Session]" has no attribute "request"
mypytest.py:13: error: Item "None" of "Optional[Session]" has no attribute "close"
Should I be doing something differently here?
You could use an explicit cast
:
from typing import cast
...
if as_tz is not sentinel:
# Never reached if as_tz has wrong type (Sentinel)
as_tz = cast(datetime.tzinfo, as_tz)
dt = dt.astimezone(as_tz)
and
new_session_made = session is None
session = cast(requests.Session, session)
You could alternately use an assert
(although this is an actual runtime check whereas the cast
is more explicitly a no-op):
assert isinstance(as_tz, datetime.tzinfo)
dt = dt.astimezone(as_tz)
and
new_session_made = session is None
assert session is not None