Search code examples
pythonpython-typingmypy

Handling conditional logic + sentinel value with mypy


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?


Solution

  • 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