3

I have been trying to validate classes that users can create in a framework style setting. I can ensure that a class attribute is present in child classes in the following manner:

from abc import ABC, abstractmethod

class A(ABC):
    @property
    @classmethod
    @abstractmethod
    def s(self):
        raise NotImplementedError

class ClassFromA(A):
    pass


ClassFromA()

Which leads to the following Exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class ClassFromA with abstract methods s

I can also check the type of the class attribute s at class creation time with a decorator, like so:

from abc import ABC, abstractmethod

def validate_class_s(cls):
    if not isinstance(cls.s, int):
        raise ValueError("S NOT INT!!!")
    return cls

class A(ABC):
    @property
    @classmethod
    @abstractmethod
    def s(self):
        raise NotImplementedError

@validate_class_s
class ClassFromA(A):
    s = 'a string'

Resulting in:

Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 3, in validate_class_s
ValueError: S NOT INT!!!

Which is useful in the eventual checking of class attributes. But this leads to verbose class definitions where each of the child classes would have to be decorated.

Is there a way to validate the class attribute (s in the examples) in the base class? Preferably not in a too verbose way?

2
  • Ideally at class definition time, just for cohesiveness sake. But if a solution is only viable at instantiation time I could adjust. Commented Oct 3, 2019 at 4:48
  • Nope, I'm trying to avoid code repetition (which is what I could do with my second solution) and ensure the type/structure of the class attribute (which I can't with my first solution). Commented Oct 3, 2019 at 12:32

3 Answers 3

7

You can use the new in Python 3.6 __init_subclass__ feature.
This is a classmethod defined on your baseclass, that will be called once for each subclass that is created, at creation time. For most asserting usecases it can be more useful than Python's ABC which will only raise an error on class instantiation time (and, conversely, if you want to subclass other abstrateclasses before getting to a concrete class, you will have to check for that on your code).

So, for example, if you want to indicate the desired methods and attributes on the subclass by making annotations on your baseclass you can do:

_sentinel = type("_", (), {})

class Base:
    def __init_subclass__(cls, **kwargs):
        errors = []
        for attr_name, type_ in cls.__annotations__.items():
            if not isinstance(getattr(cls, attr_name, _sentinel), type_):
                errors.append((attr_name, type))
        if errors:
            raise TypeError(f"Class {cls.__name__} failed to initialize the following attributes: {errors}")
        super().__init_subclass__(**kwargs)

    s: int


class B(Base):
    pass

You can put collections.abc.Callable on the annotation for requiring methods, or a tuple like (type(None), int) for an optional integer, but isinstance unfortunatelly won't work with the versatile semantics provided by the "typing" module. If you want that, I suggest taking a look at the pydantic project and make use of it.

Sign up to request clarification or add additional context in comments.

Comments

1

Another approach, with a configurable validator as a decorator you can use on several different subclasses and base classes, saving some verbosity. The base class declares the attributes using type annotation

def validate_with(baseclass):
    def validator(cls):
        for n, t in baseclass.__annotations__.items():
            if not isinstance(getattr(cls, n), t):
                raise ValueError(f"{n} is not of type {t}!!!")
        return cls
    return validator


class BaseClass:
    s: str
    i: int


@validate_with(BaseClass)
class SubClass(BaseClass):
    i = 3
    s = 'xyz'

It raises ValueError if the type doesn't match and AttributeError if the attribute is not present.

Of course you can collect the errors (as in the previous answer) and present them all in one go instead of stopping at the first error

Comments

0

Use hasattr():

class Meta(type):
def __new__(cls, name, bases, attrs):
    # Create the class
    new_class = super().__new__(cls, name, bases, attrs)
    # Ensure that the 'version' attribute is set
    if not hasattr(new_class, 'version'):
        raise TypeError(f"Class {name} must have a 'version' attribute.")
    return new_class

class X(metaclass=Meta):
    version = "1.0"  # Define the 'version' attribute

class Y(metaclass=Meta):
    version = "2.0"  # Define the 'version' attribute

# Example usage
print(f"X version: {X.version}")
print(f"Y version: {Y.version}")

# This would raise an error because 'Z' does not have a 'version' attribute
class Z(metaclass=Meta):
    pass

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.