Search code examples
pythongenericstypingmypy

mypy complains: function type annotation with Type[TypeVar['T', str, date]] and T output: Incompatible return value type (got "str", expected "date")


The intention is to overload the function and allow for multiple types of input, with user-defined coherent output.

I thus set a Type[TypeVar] of str or datetime.date (second function argument, with default value=str) where the function would output the relevant TypeVar.

The following is the basic function (that I would further extend once this version is fixed):

from typing import TypeVar, Type
from datetime import date, datetime

DateOutType = TypeVar('DateOutType', str, date)

def date2str(t: date, out_format: Type[DateOutType]=Type[str]) -> DateOutType:
    ''' Converts datetime.date to string (YYYY-MM-DD) or datetime.date output.
    '''
    if out_format is str:
        return t.strftime('%Y-%m-%d')
    elif isinstance(t, datetime):
        return t.date()
    else:
        return t

# Usage example:

dt = datetime.now()

res = date2str(dt, out_format=date)
assert type(res) == date

res = date2str(dt.date(), out_format=str)
assert type(res) == str

mypy gives error on the return statements (the TypeVar doesn't seem to work as I would have expected):

first return statement: error: Incompatible return value type (got "str", expected "date")
second return statement: error: Incompatible return value type (got "date", expected "str")
third return statement: error: Incompatible return value type (got "date", expected "str")

Any ideas? Is there a better way to write this code with proper type annotation?


Solution

  • The issue here is that mypy does not understand expressions of the form some_type is str or type(some_value) is str. You need to do either issubclass(some_type, str) or isinstance(some_value, str) instead.

    That said, it would be more cleaner in this case to just make two different functions instead:

    def date_to_str(t: date) -> str:
        return t.strftime('%Y-%m-%d')
    
    def normalize_date(t: date) -> date:
        if isinstance(t, datetime):
            return t.date()
        return t
    

    This ends up being fewer characters for your users to type since they don't need to keep including out_format=date or out_format=str everywhere. It's also less misleading: your original function doesn't actually always return a str, so calling it date2str is kind of confusing.

    It would also probably be a good idea to just delete normalize_date entirely: datetime is a subclass of date, which means it's valid to use a datetime in any place that expects just a date. This means there really shouldn't be a need to explicitly convert a datetime into a date

    For example, the following two prints would do the exact same thing (and would type check, according to mypy):

    from datetime import datetime
    
    dt = datetime.now()
    print(date_to_str(dt))
    print(date_to_str(normalize_date(dt)))