11

Even if a class is inherited from ABC, it can still be instantiated unless it contains abstract methods.

Having the code below, what is the best way to prevent an Identifier object from being created: Identifier(['get', 'Name'])?

from abc import ABC
from typing import List
from dataclasses import dataclass

@dataclass
class Identifier(ABC):
    sub_tokens: List[str]

    @staticmethod
    def from_sub_tokens(sub_tokens):
        return SimpleIdentifier(sub_tokens) if len(sub_tokens) == 1 else CompoundIdentifier(sub_tokens)


@dataclass
class SimpleIdentifier(Identifier):
    pass


@dataclass
class CompoundIdentifier(Identifier):
    pass
Hlib Babii
  • 599
  • 1
  • 7
  • 24

2 Answers2

20

You can create a AbstractDataclass class which guarantees this behaviour, and you can use this every time you have a situation like the one you described.

@dataclass 
class AbstractDataclass(ABC): 
    def __new__(cls, *args, **kwargs): 
        if cls == AbstractDataclass or cls.__bases__[0] == AbstractDataclass: 
            raise TypeError("Cannot instantiate abstract class.") 
        return super().__new__(cls)

So, if Identifier inherits from AbstractDataclass instead of from ABC directly, modifying the __post_init__ will not be needed.

@dataclass
class Identifier(AbstractDataclass):
    sub_tokens: List[str]

    @staticmethod
    def from_sub_tokens(sub_tokens):
        return SimpleIdentifier(sub_tokens) if len(sub_tokens) == 1 else CompoundIdentifier(sub_tokens)


@dataclass
class SimpleIdentifier(Identifier):
    pass


@dataclass
class CompoundIdentifier(Identifier):
    pass

Instantiating Identifier will raise TypeError but not instantiating SimpleIdentifier or CompountIdentifier. And the AbstractDataclass can be re-used in other parts of the code.

Jundiaius
  • 6,214
  • 3
  • 30
  • 43
  • What about cases when `SimpleIdentifier` or `CompoundIdentifier` are also meant to be abstract (concrete classes are not immediate derivatives of `AbstractDataclass`)? – z33k Oct 25 '21 at 07:56
  • 1
    @z33k I guess you can use multiple inheritance so that they both inherit from `AbstractDataclass` too. – abdelgha4 Oct 25 '22 at 15:07
12

The easiest way I have found is to check the type of the object in the __post_init__ method:

@dataclass
class Identifier(ABC):
    ...

    def __post_init__(self):
        if self.__class__ == Identifier:
            raise TypeError("Cannot instantiate abstract class.")

    ...
Hlib Babii
  • 599
  • 1
  • 7
  • 24
  • This doesn't work if you need to use initialized class variables inside `__init__`. You won't be able to catch the `AttributeError` – Angelo van Meurs Aug 12 '23 at 13:45