Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dataclass argument choices with a default option

I'm making a dataclass with a field for which I'd like there to only be a few possible values. I was thinking something like this:

@dataclass
class Person:
   name: str = field(default='Eric', choices=['Eric', 'John', 'Graham', 'Terry'])

I know that one solution is to validate arguments in the __post_init__ method but is there a cleaner way using something like the syntax above?

like image 745
idle Avatar asked Dec 22 '22 18:12

idle


2 Answers

python 3.8 introduced a new type called Literal that can be used here:

from dataclasses import dataclass
from typing import Literal

@dataclass
class Person:
    name: Literal['Eric', 'John', 'Graham', 'Terry'] = 'Eric'

Type checkers like mypy have no problems interpreting it correctly, Person('John') gets a pass, and Person('Marc') is marked as incompatible. Note that this kind of hint requires a type checker in order to be useful, it won't do anything on its own when you're just running the code.

If you're on an older python version and can't upgrade to 3.8, you can also get access to the Literal type through the official pip-installable backport package typing-extensions, and import it with from typing_extensions import Literal instead.


If you need to do actual checks of the passed values during runtime, you should consider using pydantic to define your dataclasses instead. Its main goal is to extend on dataclass-like structures with a powerful validation engine which will inspect the type hints in order to enforce them, i.e. what you considered to write by hand in the __post_init__.

like image 111
Arne Avatar answered Jan 09 '23 02:01

Arne


Works in Python 3.8 (typing.Literal):

from dataclasses import dataclass
from typing import Literal

from validated_dc import ValidatedDC


@dataclass
class Person(ValidatedDC):
    name: Literal['Eric', 'John', 'Graham', 'Terry'] = 'Eric'


# Validation during instance creation

eric = Person()
assert eric.name == 'Eric'
assert eric.get_errors() is None

john = Person('John')
assert john.get_errors() is None

peter = Person('Peter')  # <-- Invalid value!
assert peter.get_errors()
print(peter.get_errors())
# {'name': [
#     LiteralValidationError(
#         literal_repr='Peter', literal_type=<class 'str'>,
#         annotation=typing.Literal['Eric', 'John', 'Graham', 'Terry']
#     )
# ]}

# You can check at any time

assert john.is_valid()  # Starts validation and returns True or False

john.name = 'Ivan'  # <-- Invalid value!
assert not john.is_valid()
print(john.get_errors())
# {'name': [
#     LiteralValidationError(
#         literal_repr='Ivan', literal_type=<class 'str'>,
#         annotation=typing.Literal['Eric', 'John', 'Graham', 'Terry']
#     )
# ]}

john.name = 'John'  # <-- Valid value
assert john.is_valid()
assert john.get_errors() is None

ValidatedDC: https://github.com/EvgeniyBurdin/validated_dc

like image 31
Evgeniy_Burdin Avatar answered Jan 09 '23 04:01

Evgeniy_Burdin