Class and instance methods live in the same namespace and you cannot reuse names like that; the last definition of id will win in that case.
The class method will continue to work on instances however, there is no need to create a separate instance method; just use:
class X:
@classmethod
def id(cls):
return cls.__name__
because the method continues to be bound to the class:
>>> class X:
... @classmethod
... def id(cls):
... return cls.__name__
...
>>> X.id()
'X'
>>> X().id()
'X'
This is explicitly documented:
It can be called either on the class (such as C.f()) or on an instance (such as C().f()). The instance is ignored except for its class.
If you do need distinguish between binding to the class and an instance
If you need a method to work differently based on where it is being used on; bound to a class when accessed on the class, bound to the instance when accessed on the instance, you'll need to create a custom descriptor object.
The descriptor API is how Python causes functions to be bound as methods, and bind classmethod objects to the class; see the descriptor howto.
You can provide your own descriptor for methods by creating an object that has a __get__ method. Here is a simple one that switches what the method is bound to based on context, if the first argument to __get__ is None, then the descriptor is being bound to a class, otherwise it is being bound to an instance:
class class_or_instancemethod(classmethod):
def __get__(self, instance, type_):
descr_get = super().__get__ if instance is None else self.__func__.__get__
return descr_get(instance, type_)
This re-uses classmethod and only re-defines how it handles binding, delegating the original implementation for instance is None, and to the standard function __get__ implementation otherwise.
Note that in the method itself, you may then have to test, what it is bound to. isinstance(firstargument, type) is a good test for this:
>>> class X:
... @class_or_instancemethod
... def foo(self_or_cls):
... if isinstance(self_or_cls, type):
... return f"bound to the class, {self_or_cls}"
... else:
... return f"bound to the instance, {self_or_cls"
...
>>> X.foo()
"bound to the class, <class '__main__.X'>"
>>> X().foo()
'bound to the instance, <__main__.X object at 0x10ac7d580>'
An alternative implementation could use two functions, one for when bound to a class, the other when bound to an instance:
class hybridmethod:
def __init__(self, fclass, finstance=None, doc=None):
self.fclass = fclass
self.finstance = finstance
self.__doc__ = doc or fclass.__doc__
# support use on abstract base classes
self.__isabstractmethod__ = bool(
getattr(fclass, '__isabstractmethod__', False)
)
def classmethod(self, fclass):
return type(self)(fclass, self.finstance, None)
def instancemethod(self, finstance):
return type(self)(self.fclass, finstance, self.__doc__)
def __get__(self, instance, cls):
if instance is None or self.finstance is None:
# either bound to the class, or no instance method available
return self.fclass.__get__(cls, None)
return self.finstance.__get__(instance, cls)
This then is a classmethod with an optional instance method. Use it like you'd use a property object; decorate the instance method with @<name>.instancemethod:
>>> class X:
... @hybridmethod
... def bar(cls):
... return f"bound to the class, {cls}"
... @bar.instancemethod
... def bar(self):
... return f"bound to the instance, {self}"
...
>>> X.bar()
"bound to the class, <class '__main__.X'>"
>>> X().bar()
'bound to the instance, <__main__.X object at 0x10a010f70>'
Personally, my advice is to be cautious about using this; the exact same method altering behaviour based on the context can be confusing to use. However, there are use-cases for this, such as SQLAlchemy's differentiation between SQL objects and SQL values, where column objects in a model switch behaviour like this; see their Hybrid Attributes documentation. The implementation for this follows the exact same pattern as my hybridmethod class above.
Here are the type-hinted versions of the above, per request. These require that your project has typing_extensions installed:
from typing import Generic, Callable, TypeVar, overload
from typing_extensions import Concatenate, ParamSpec, Self
_T = TypeVar("_T")
_R_co = TypeVar("_R_co", covariant=True)
_R1_co = TypeVar("_R1_co", covariant=True)
_R2_co = TypeVar("_R2_co", covariant=True)
_P = ParamSpec("_P")
class class_or_instancemethod(classmethod[_T, _P, _R_co]):
def __get__(
self, instance: _T, type_: type[_T] | None = None
) -> Callable[_P, _R_co]:
descr_get = super().__get__ if instance is None else self.__func__.__get__
return descr_get(instance, type_)
class hybridmethod(Generic[_T, _P, _R1_co, _R2_co]):
fclass: Callable[Concatenate[type[_T], _P], _R1_co]
finstance: Callable[Concatenate[_T, _P], _R2_co] | None
__doc__: str | None
__isabstractmethod__: bool
def __init__(
self,
fclass: Callable[Concatenate[type[_T], _P], _R1_co],
finstance: Callable[Concatenate[_T, _P], _R2_co] | None = None,
doc: str | None = None,
):
self.fclass = fclass
self.finstance = finstance
self.__doc__ = doc or fclass.__doc__
# support use on abstract base classes
self.__isabstractmethod__ = bool(getattr(fclass, "__isabstractmethod__", False))
def classmethod(self, fclass: Callable[Concatenate[type[_T], _P], _R1_co]) -> Self:
return type(self)(fclass, self.finstance, None)
def instancemethod(self, finstance: Callable[Concatenate[_T, _P], _R2_co]) -> Self:
return type(self)(self.fclass, finstance, self.__doc__)
@overload
def __get__(self, instance: None, cls: type[_T]) -> Callable[_P, _R1_co]: ...
@overload
def __get__(self, instance: _T, cls: type[_T] | None = ...) -> Callable[_P, _R1_co] | Callable[_P, _R2_co]: ...
def __get__(self, instance: _T, cls: type[_T] | None = None) -> Callable[_P, _R1_co] | Callable[_P, _R2_co]:
if instance is None or self.finstance is None:
# either bound to the class, or no instance method available
return self.fclass.__get__(cls, None)
return self.finstance.__get__(instance, cls)