2

If I have for example:

class Parent(object):
    @property
    @abc.abstractmethod
    def asdf(self) -> str:
        """ Must be implemented by child """

@dataclass
class Children(Parent):
    asdf = "1234"
    
    def some_method(self):
        self.asdf = "5678"

I get an error from mypy saying I am shadowing class attribute in some_method. This is understandable, since asdf is defined in Parent and so becomes a class attribute. But, if I do this instead:

class Parent(object):
    asdf: str

No error is generated by mypy, but then the assignment to asdf is not enforced. And also, asdf is still nonetheless a class attribute right? Are class attributes in general not meant to be overridden by methods in children? How can I make a parent class that enforces its children to have a certain instance attribute (and not class attributes)?

If I look at other languages like C#, I think these 'class attributes' would be kind of equivalent to properties, but there we can customize its get and set behavior. Why is it not the case with Python?

anthony sottile
  • 61,815
  • 15
  • 148
  • 207
aldo
  • 125
  • 1
  • 7
  • flake8 does not produce an error here – anthony sottile Mar 06 '23 at 15:10
  • Note that `asdf = "1234"` provides an actual definition of the abstract "method" (`ABC` doesn't care about types, only actual bound values), while `asdf: str` does not. The latter simply adds an annotation to the class object that the `dataclass` decorator uses to construct a field for use in constructing various methods. – chepner Mar 06 '23 at 15:19
  • The dataclass confuses this a bit: is `asdf` supposed to be a property, or an instance attribute, or something else? Do you want a read-only attribute, or an attribute that *defaults* to `1234` but can be set by something else? You may want to define `Parent.__init_subclass__` instead of using `abc`. (`__init_subclass__` can do pretty much anything the `abc` module can do, but doesn't require a metaclass that can interfere with other, more necessary, metaclasses. `abc` just came first, and provides a simpler interface for its intended use cases.) – chepner Mar 06 '23 at 15:35

1 Answers1

1

You might be misunderstanding the purpose of the @property decorator. Generally it's used for a function that is supposed to be accessed / "feel like" a constant to outside code.

Python does not do strict type-checking of variables without significant effort. (It does constrain the types of values at runtime, so a variable's type won't change subtly / unexpectedly. However, it will normally allow a string to be passed / returned in any place nominally expecting an integer - or vice versa. The value which is passed at runtime will continue to be used, without conversion as its original type, and without an error being thrown - until & unless a conversion is required & not provided. For example, type hints marking a variable as a string don't prevent assignment of a float to that variable. An error will only be thrown if there's an explicit type check, or if you try to call a method defined for strings but not for floats - e.g. str.join().)

The grounding assumption in that is "other developers know what they're doing as much as the current developer does", so you have to do some workarounds to get strict type enforcement of the type you'd see in C#, Java, Scala and so on. Think of the "type hints" more like documentation and help for linters and the IDE than strict type enforcement.

This point of view gives a few alternatives, depending on what you want from Children.asdf: How strict should the check be? Is the class-constant string you've shown what you're looking for, or do you want the functionality normally associated with the @property decorator?

First draft, as a very non-strict constant string, very much in Python's EAFP tradition:

class Parent:
    ASDF: str = None # Subclasses are expected to define a string for ASDF.

class Children(Parent):
    ASDF = 'MyChildrenASDF'

If you want to (somewhat) strictly enforce that behavior, you could do something like this:

class Parent:
    ASDF: str = None # Subclasses are required to define a string for ASDF.
 
    def __init__(self):
        if not isinstance(self.ASDF, str):
            # This uses the class name of Children or whatever 
            # subclasses Parent in the error message, which makes 
            # debugging easier if you have many subclasses and multiple 
            # developers.
            raise TypeError(f'{self.__class__.__name__}.ASDF must be a string.')

class Children(Parent):
    ASDF = 'MyChildrenASDF'

    def __init__(self):
        # This approach does assume the person writing the subclass
        # remembers to call super().__init__().  That's not enforced
        # automatically.
        super().__init__()

The second option is about as strict as I'd go personally, except in rare circumstances. If you need greater enforcement, you could write a unit test which loops over all Parent.__subclasses__(), and performs the check each time tests are run.

Alternately, you could define a Python metaclass. Note that metaclasses are an "advanced topic" in Python, and the general rule of thumb is "If you don't know whether you need a metaclass, or don't know what a metaclass is, you shouldn't use a metaclass". Basically, metaclasses let you hack the class-definition process: You can inject class attributes which are automatically defined on the fly, throw errors if things aren't defined, and all sorts of other wacky tricks... but it's a deep rabbit hole and probably overkill for most use cases.

If you want something which is actually a function, but uses the @property decorator so it feels like an instance property, you could do this:

class Parent:
    @property
    def asdf(self) -> str:
         prepared = self._calculate_asdf()
         if not isinstance(prepared):
            raise TypeError(f'{self.__class__.__name__}._calculate_asdf() must return a string.')

    def _calculate_asdf(self) -> str:
         raise NotImplementedError(f'{self.__class__.__name__}._calculate_asdf() must return a string.')

class Children(Parent):
    def _calculate_asdf(self) -> str:
        # I needed a function here to show what the `@property` could
        # do. This one seemed convenient. Any function returning a
        # string would be fine.
        return self.__class__.__name__.reverse()
Sarah Messer
  • 3,592
  • 1
  • 26
  • 43
  • 2
    Nit: Python is strongly typed, so it is quite particular about what types are used at *runtime*. What it does not do is *static* type checking, where *names* are picky about what types of values can be assigned to them. – chepner Mar 06 '23 at 15:22
  • Okay @chepner, I agree "strong" and "strict" have a subtle distinction I was overlooking: https://stackoverflow.com/questions/11328920/is-python-strongly-typed I'll update the A to acknowledge and link this, but the exact distinction feels like a bit of a distraction from both OP's Q and my A. I used "strict", not "strong". If my edits are unsatisfactory, please let me know. – Sarah Messer Mar 06 '23 at 15:51
  • 1
    I'm not entirely sure what OP wants, so I'll hold off comment on the rest of the answer (which looks good, assuming various interpretations of the OP's intent). I would note that `__init_subclass__` can be a cleaner alternative to a metaclass, depending on what you were thinking of having the metaclass do. – chepner Mar 06 '23 at 16:04