10

I'm trying out using Python's type annotations with abstract class. My __init__ function looks like this:

from abc import ABCMeta

class SomeClass(object, metaclass=ABCMeta):
    def __init__(self, *args, **kwargs):
        print("Initiating %s object.", self.__class__.__name__)

        self.username = kwargs['data']
        assert isinstance(self.username, str)

        is_premioum = kwargs.get('premioum', False)

        self.money_investmant = kwargs.get('investmant')
        if isinstance(self.money_investmant, str):
            self.money_investmant = float(self.money_investmant)

As you can see, kwargs could contain arguments from a several number of types- float, bool and str.

Now, I am trying to write the type annotation for the function, that looks like this:

def __init__(self, *args, **kwargs: Union[bool, str, float]) -> None:

But my PyCharm IDE alerts me:

Except type 'Integral', got 'str' instead

And:

Cannot find referance 'get' in bool | str | float'

Am I doing something wrong?

How should I write the type annotation for kwargs if it contains arguments from multiple types?

Yuval Pruss
  • 8,716
  • 15
  • 42
  • 67

2 Answers2

4

See this bug and this bug on the issue tracker for PyCharm. This is apparently an issue with PyCharm's checker; mypy (another type checker for Python) does not complain when I execute similar code.

There's already a fix for this and, it's apparently available in build 171.2014.23. Until then, I'd suppose Any would suffice as a temporary workaround to get the checker to stop complaining.

Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253
2

If one wants to describe specific named arguments expected in kwargs, one can instead pass in a TypedDict which defines required and optional parameters. Optional parameters are what were the kwargs:

This allows one to have unset (NOT None default) optional arguments AND have type hints on them.

This is also useful if they keys have invalid python variable names because TypedDict is the only way to define the types of those values aside from the very general **kwargs value type hinting.

import typing
from abc import ABCMeta


class RequiredProps(typing.TypedDict):
    # all of these must be present
    data: str

class OptionalProps(typing.TypedDict, total=False):
    # these can be included or they can be omitted
    premium: bool
    investment: typing.Union[str, float]

class ReqAndOptional(RequiredProps, OptionalProps):
    pass

class SomeClass(object, metaclass=ABCMeta):
    def __init__(self, *args, kwargs: ReqAndOptional):
        print("Initiating %s object.", self.__class__.__name__)

        self.username = kwargs['data']
        assert isinstance(self.username, str)

        is_premium = kwargs.get('premium', False)
        assert isinstance(is_premium, bool)

        self.money_investment = kwargs.get('investment')
        assert isinstance(elf.money_investment, (str, float))
        if isinstance(self.money_investment, str):
            self.money_investment = float(self.money_investment)
spacether
  • 2,136
  • 1
  • 21
  • 28
  • 1
    Is this not an excessive amount of boilerplate required to support _a really common pattern_? – Thismatters Apr 30 '21 at 12:56
  • Yes it is a bit of boilerplate. The advantage is that one cane have unset optional arguments with type hints. If one uses normal kwargs, one lacks type hints for them. If ne uses None defaults then one loads in None value when one may not want to do that. – spacether Apr 30 '21 at 15:50
  • 2
    Is it possible to get it working with `**kwargs` instead of `kwargs` in `__init__`? – Carlos Pinzón Nov 28 '21 at 15:12
  • Just checked this with pycharm and the answer is no. When one adds a type hint for **kwargs pycharm at least assumes that you are defining the type of the values not the total dict, please read https://stackoverflow.com/a/63550734/4175822 – spacether Dec 22 '21 at 18:01