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?
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__
.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With