Can I use __new__ for implementing an abstact base class?

I have a base class which aims at providing common implementation and this class should not be instantiated directly. For the time being this class does not have any abstract methods. Therefore even if I declare it as abstract, someone can instantiate it (see the example below):

import abc

class BaseURL(metaclass=abc.ABCMeta):

    def __init__(self, path: str) -> None:
        self.path = path

    def __init_subclass__(cls, schema: str) -> None:
        cls.schema = schema

    def __str__(self) -> str:
        return f"{self.schema}://{self.path}"

class FileURL(BaseURL, schema="file")

    def __init__(self, filepath: str) -> None:
        if not filepath:
            raise ValueError(f'Invalid filepath "{filepath}" for File URL schema.')
        super().__init__(filepath)

Let’s say that the example, above, is part of an API. My intention is to allow the API user create URLs only by using the subclasses of BaseURL (e.g. FileURL). However, using the code above, nothing prevents someone from creating a URL using BaseURL.

Initially I thought about decorating BaseURL.__init__ with abc.abstractmethod but this leads to another implication which I don’t like, as every subclass of BaseURL must override __init__.

I started searching for an answer and I found one (on stackoverflow) which proposes the use of __new__. So, using this approach the code above becomes like the following:

import abc

class BaseURL(metaclass=abc.ABCMeta):

    def __new__(cls, *args, **kwargs):
        if cls is BaseURL:
            raise TypeError(f"Can't instantiate abstract class {cls.__name__}.")
        return super().__new__(cls)

    def __init__(self, path: str) -> None:
        self.path = path

    def __init_subclass__(cls, schema: str) -> None:
        cls.schema = schema

    def __str__(self) -> str:
        return f"{self.schema}://{self.path}"

class FileURL(BaseURL, schema="file")

    def __init__(self, filepath: str) -> None:
        if not filepath:
            raise ValueError(f'Invalid filepath "{filepath}" for File URL schema.')
        super().__init__(filepath)

I’ve tested this approach and it works. However, I’m not sure if this is a legitimate solution and for that reason I’m opening the current topic.

So, any thoughts, ideas, concerns are welcome and will be greatly appreciated :slight_smile:

I’m pretty sure the usual intended solution for this is to mark __init__ as abstract.

If you want every class to have a path attribute and a common signature for __init__, one approach is to use the template-and-hook pattern:

class BaseURL(metaclass=abc.ABCMeta):

    def __init__(self, path: str) -> None:
        self.path = path
        self._init_subclass()

    @abstractmethod
    def _init_subclass(self) -> None:
        """Use this to do any subclass-specific initialization."""

That also avoids the need for the super call (and remembering about it) in the subclasses.

Your approach seems… acceptable to me, but I’m not sure what additional benefit you’ll gain from abc.ABCMeta if you take this path.