'Creating value type that raises exception on reading

How do I create a value type that raises an exception when read?

For example:

from dataclasses import dataclass, field

Missing = ...


@dataclass
class A:
    a: int = field(default=None)  # <- value can be None
    b: int = field(default=Missing)  # <- can be Missing until you try to access it

    def print(self):
        for i in [self.a, self.b]:
            print(i)  # <- raises ValueError if i is Missing


Solution 1:[1]

In Python, it seems there is always a way for anything :-)

It appears you can solve this by a clever use of a descriptor value as a dataclass field, as illustrated below. I would also read more on the section on Validators to understand a little bit more about how descriptors work.

from dataclasses import dataclass


# create `_MissingType` class
_MissingType = type('_MissingType', (), {'__bool__': lambda self: False})

# create a singleton for that class
Missing = _MissingType()


class MissingValidator:
    __slots__ = ('default', 'private_name')

    # You may or may not want a default value
    def __init__(self, default=Missing):
        self.default = default

    def __set_name__(self, owner, name):
        self.private_name = '_' + name

    # override __get__() to return a default value if one is not passed in to __init__()
    def __get__(self, obj, obj_type=None):
        try:
            value = getattr(obj, self.private_name)

            if value is Missing:
                cls_name = obj_type.__qualname__
                public_name = self.private_name.lstrip('_')
                raise ValueError(f'Missing value for field `{public_name}` in class `{cls_name}`')

            return value

        except AttributeError:
            return self.default

    def __set__(self, obj, value):
        setattr(obj, self.private_name, value)


@dataclass
class A:
    a: int = None  # <- value can be None
    b: int = MissingValidator()  # <- can be Missing until you try to access it

    def print(self):
        for i in [self.a, self.b]:
            print(i)  # <- raises ValueError if i is Missing


A(b=3).print()
# None
# 3

A(a=42).print()
# raises:
#   ValueError: Missing value for field `b` in class `A`

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1