[Python] Easy argument type check with dataclass

Introduction

--When I was practicing writing code with DDD, I tried and errored the type check method of ValueObject, so a memo at that time --As an article practice for Qiita (first post).

Conclusion

--Since the processing at the time of initialization is applied by __post_init__ (), type check is executed here. --Type check with is instance --Get the expected value type from self.__annotations__ --Convert instance to dict with dataclasses.asdict (self). Multiply this by is instance.

What is a data class?

--init () is automatically generated. --You don't need to put an argument in __init __ () and do self.hoge = arg_hoge. --Suitable for creating ValueObjects.

Normal writing


class Users:
    def __init__(self, user_name: str, user_id: int):
        self.user_name = user_name
        self.user_id = user_id

How to write in data class


class User:
    user_name: str
    user_id: int

It's better to write in dataclass!

Can you type check with dataclass?

This is the main subject.

It's typed with ʻuser_name: str or ʻuser_id: int, and it looks like it's type-checked, but it's actually a normal annotation.

Even though it is specified as str type, it is entered as int type.

code


import dataclasses

@dataclasses.dataclass(frozen=True)
class User:
    user_name: str
    user_id: int

c = User(user_name='Fletchinder', user_id=1)
print(f'{c.user_id}:{c.user_name}')

c_fail = User(user_name=2, user_id='Talonflame')
print(f'{c_fail.user_id}:{c_fail.user_name}')

Execution result


> python .\dataclass test.py
1:Fletchinder
Talonflame:2

Anything can be put in as above.

Perform type checking at initialization with __post_init__ ()

By defining the __post_init__ () function in the dataclass as shown below, you can write the processing at the time of initialization. Let's type check here using ʻis instance`


@dataclasses.dataclass(frozen=True)
class User:
    user_name: str
    user_id: int

    def __post_init__(self):
        if not isinstance(self.user_name, str):
            raise Exception
        if not isinstance(self.user_id, int):
            raise Exception

Execution result


> python .\dataclass test.py
1:Fletchinder
Traceback (most recent call last):
  File ".\dataclass test.py", line 17, in <module>
    c_fail = User(user_name=2, user_id='Talonflame')
  File "<string>", line 4, in __init__
  File ".\dataclass test.py", line 10, in __post_init__
    raise Exception
Exception

As expected, I was able to make an exception!

Type checking is difficult when there are many variables

In the examples so far, there were two, ʻuser_name and ʻuser_id, but it is difficult if there are many. Therefore, I want to limit the number of type checks written in __post_init__ to the number of variables.

That's why I wrote the following.


@dataclasses.dataclass(frozen=True)
class User:
    user_name: str
    user_id: int

    def __post_init__(self):
        # 1.Convert User instance to dict type with asdict
        user_dict = dataclasses.asdict(self)
        # 2. self.__annotations__Get the expected value type from
        #    self.__annotations__Contains the name of the argument and the type it specifies as a dict.
        #From now on, get the type of the expected value and check the type with isinstance.
        for user_arg_name, user_arg_expected_type in self.__annotations__.items():
            # 3.is instance execution
            #From the User converted to dict type, specify the target variable with the Key of the annotation and execute it.
            if not isinstance(user_dict[user_arg_name], user_arg_expected_type):
                print(f'{user_arg_name} is not ok')
                raise Exception
            else:
                print(f'{user_arg_name} is ok')

I put in the details in the comments. Get all the arguments (variables) that the instance has with ʻasdict, get the expected value type with self.__ annotations__, and multiply by ʻis instance. Excluding comment out and print, you can write in about 4 or 5 lines

Execution result

> python .\dataclass test.py
user_name is ok
user_id is ok
1:Fletchinder
user_name is not ok
Traceback (most recent call last):
  File ".\dataclass test.py", line 21, in <module>
    c_fail = User(user_name=2, user_id='Talonflame')
  File "<string>", line 4, in __init__
  File ".\dataclass test.py", line 13, in __post_init__
    raise Exception
Exception

Weaknesses of this code

If you specify the type with typing etc., this method will not work. The following is the one in which the argument type is specified by List [int] without comment.


import dataclasses
from typing import List 

@dataclasses.dataclass(frozen=True)
class User:
    user_name: str
    user_id: int
    status_list: List[int]

    def __post_init__(self):
        user_dict = dataclasses.asdict(self)
        for user_arg_name, user_arg_expected_type in self.__annotations__.items():
            if not isinstance(user_dict[user_arg_name], user_arg_expected_type):
                print(f'{user_arg_name} is not ok')
                raise Exception
            else:
                print(f'{user_arg_name} is ok')

status_list=[50,51]

c = User(user_name='Fletchinder', user_id=1, status_list=status_list)
print(f'{c.user_id}:{c.user_name}')

c_fail = User(user_name=2, user_id='Talonflame', status_list=status_list)
print(f'{c_fail.user_id}:{c_fail.user_name}')

I'm getting an error in the list as shown below.

> python .\dataclass test.py
user_name is ok
user_id is ok
Traceback (most recent call last):
  File ".\dataclass test.py", line 27, in <module>
    c = User(user_name='Fletchinder', user_id=1, status_list=status_list)
  File "<string>", line 5, in __init__
  File ".\dataclass test.py", line 19, in __post_init__
    if not isinstance(user_dict[user_arg_name], user_arg_expected_type):
  File "C:\Users\proje\AppData\Local\Programs\Python\Python37\lib\typing.py", line 708, in __instancecheck__
    return self.__subclasscheck__(type(obj))
  File "C:\Users\proje\AppData\Local\Programs\Python\Python37\lib\typing.py", line 716, in __subclasscheck__
    raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks

Why not

As you can see by doing the following, List [int] is a type for specifying the type, so it is not the actual list type.

>>> from typing import List 
>>> print(type(List[dir]))  
<class 'typing._GenericAlias'>

It seems that it is necessary to convert at the time of type check.

in conclusion

Type checking using __post_init__ seems fine. However, if you are using typing when specifying the type, it seems that you need to devise another way to check it by turning it with a for statement. It may be possible to check from the outside instead of putting it in the code with mypy etc. (If the environment is solid, such as automatic confirmation at the time of push, this is also available)

reference

--Python Documentation contents dataclasses --- Dataclasses

Recommended Posts

[Python] Easy argument type check with dataclass
Domain check with Python
Check Python # type identity
Check version with python
[Co-occurrence analysis] Easy co-occurrence analysis with Python! [Python]
Check python coverage with pytest-cov
Python --Check type of values
Easy folder synchronization with Python
Easy Python compilation with NUITKA-Utilities
Easy HTTP server with Python
[Python] Easy parallel processing with Joblib
Function argument type definition in python
Easy Python + OpenCV programming with Canopy
Easy email sending with haste python3
Bayesian optimization very easy with Python
Master the type with Python [Python 3.9 compatible]
Easy data visualization with Python seaborn.
Easy parallel execution with python subprocess
Easy modeling with Blender and Python
Easy keyword extraction with TermExtract for Python
[Python] Super easy test with assert statement
Check the existence of the file with python
Easy introduction of speech recognition with Python
Check stock prices with slackbot using python
Receive date type (datetime) with ArgumentParser [python]
[Easy Python] Reading Excel files with openpyxl
Easy web app with Python + Flask + Heroku
Easy image processing in Python with Pillow
[Easy Python] Reading Excel files with pandas
Easy web scraping with Python and Ruby
[Python] Easy Reinforcement Learning (DQN) with Keras-RL
[Python] Easy introduction to machine learning with python (SVM)
FizzBuzz with Python3
Csv output from Google search with [Python]! 【Easy】
Scraping with Python
Python nan check
Python is easy
Python numeric type
Statistics with python
Read data with python / netCDF> nc.variables [] / Check data size
Automatically check Python scripts with GitHub + Travis-CI + pycodestyle
python grammar check
Scraping with Python
Python with Go
Python> Run-time argument> Check if -d is attached
Twilio with Python
Integrate with Python
Check the date of the flag duty with Python
Play with 2016-Python
AES256 with python
Tested with Python
python starts with ()
with syntax (Python)
Bingo with python
Zundokokiyoshi with python
Python2 string type
Python # string type
[Python] Determine the type of iris with SVM
Easy Lasso regression analysis with Python (no theory)
Excel with Python
Make your Python environment "easy" with VS Code