__init__
is not responsible for creating a instance. It is a hook method that Python calls for you after the instance is already created. You can't prevent instance creation from there. Besides, you don't want to prevent all instance creation, even your classmethod
has to create an instance at some point.
Since all you want to do is raise an exception when your factory method is not used to create the instance, it's still fine to raise an exception in __init__
method. That'll prevent the new instance from being assigned anywhere. What you need to do then is distinguish between direct access, and your factory method being used.
You could achieve this is several different ways. You could use a "secret" token that only the factory method passes in:
_token = object() # unique token to flag factory use
class A:
def __init__(self, data, _from_factory=None):
if _from_factory is not _token:
raise TypeError(f"Can't create {type(self).__name__!r} objects directly")
self._data = data
@classmethod
def create_from_file(cls, file):
data = file.read()
return cls(data, _from_factory=_token)
The classmethod
still creates an instance, the __init__
is still called for that instance, and no exception is raised because the right token was passed in.
You could make your class an implementation detail of the module and only provide a public factory function:
def create_from_file(cls, file):
data = file.read()
return _A(data)
class _A:
def __init__(self, data):
self._data = data
Now the public API only gives you create_from_file()
, the leading underscore tells developers that _A()
is an internal name and should not be relied on outside of the module.
Actual instance creation is the responsibility of the object.__new__
method; you could also use that method to prevent new instances to be created. You could use the same token approach as I showed above, or you could bypass it altogether by using super()
to call the original overridden implementation:
class A:
def __new__(cls, *args, **kwargs):
raise TypeError(f"Can't create {cls.__name__!r} objects directly")
def __init__(self, data):
self._data = data
@classmethod
def create_from_file(cls, file):
data = file.read()
# Don't use __new__ *on this class*, but on the next one in the
# MRO. We'll have to manually apply __init__ now.
instance = super().__new__(cls)
instance.__init__(data)
return instance
Here a direct call to A()
will raise an exception, but by using super().__new__
in the classmethod
we bypass the A.__new__
implementation.
Note: __new__
is implicitly made a staticmethod
, so we have to manually pass in the cls
argument when we call it from the classmethod
.