5.5 KiB
Methods
Background: Functions as descriptors
Say we have a simple class C with a function definition f inside its body:
class C:
def f(self, x: int) -> str:
return "a"
Whenever we access the f attribute through the class object itself (C.f) or through an instance
(C().f), this access happens via the descriptor protocol. Functions are (non-data) descriptors
because they implement a __get__ method. This is crucial in making sure that method calls work as
expected. In general, the signature of the __get__ method in the descriptor protocol is
__get__(self, instance, owner). The self argument is the descriptor object itself (f). The
passed value for the instance argument depends on whether the attribute is accessed from the class
object (in which case it is None), or from an instance (in which case it is the instance of type
C). The owner argument is the class itself (C of type Literal[C]). To summarize:
C.fis equivalent togetattr_static(C, "f").__get__(None, C)C().fis equivalent togetattr_static(C, "f").__get__(C(), C)
Here, inspect.getattr_static is used to bypass the descriptor protocol and directly access the
function attribute. The way the special __get__ method on functions works like is as follows. In
the former case, if the instance attribute is None, it simply returns the function itself. In
the latter case, it returns a bound method object:
from inspect import getattr_static
reveal_type(getattr_static(C, "f")) # revealed: Literal[f]
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: Literal[f]
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: <bound method `f` of `C`>
In conclusion, this is why we see the following two types when accessing the f attribute on the
class object C and on an instance C():
reveal_type(C.f) # revealed: Literal[f]
reveal_type(C().f) # revealed: <bound method `f` of `C`>
A bound method is a callable object that contains a reference to the instance that it was called
on (can be inspected via __self__), and the function object that it refers to (can be inspected
via __func__):
bound_method = C().f
reveal_type(bound_method.__self__) # revealed: C
reveal_type(bound_method.__func__) # revealed: Literal[f]
When we call the bound method, the instance is implicitly passed as the first argument (self):
reveal_type(C().f(1)) # revealed: str
reveal_type(bound_method(1)) # revealed: str
When we call the function object itself, we need to pass the instance explicitly:
C.f(1) # error: [missing-argument]
reveal_type(C.f(C(), 1)) # revealed: str
When we access methods from derived classes, they will be bound to instances of the derived class:
class D(C):
pass
reveal_type(D().f) # revealed: <bound method `f` of `D`>
If we access an attribute on a bound method object itself, it will defer to types.MethodType:
reveal_type(bound_method.__hash__) # revealed: <bound method `__hash__` of `MethodType`>
Basic method calls on class objects and instances
class Base:
def method_on_base(self, x: int | None) -> str:
return "a"
class Derived(Base):
def method_on_derived(self, x: bytes) -> tuple[int, str]:
return (1, "a")
reveal_type(Base().method_on_base(1)) # revealed: str
reveal_type(Base.method_on_base(Base(), 1)) # revealed: str
Base().method_on_base("incorrect") # error: [invalid-argument-type]
Base().method_on_base() # error: [missing-argument]
Base().method_on_base(1, 2) # error: [too-many-positional-arguments]
reveal_type(Derived().method_on_base(1)) # revealed: str
reveal_type(Derived().method_on_derived(b"abc")) # revealed: tuple[int, str]
reveal_type(Derived.method_on_base(Derived(), 1)) # revealed: str
reveal_type(Derived.method_on_derived(Derived(), b"abc")) # revealed: tuple[int, str]
Method calls on literals
Boolean literals
reveal_type(True.bit_length()) # revealed: int
reveal_type(True.as_integer_ratio()) # revealed: tuple[int, Literal[1]]
Integer literals
reveal_type((42).bit_length()) # revealed: int
String literals
reveal_type("abcde".find("abc")) # revealed: int
reveal_type("foo".encode(encoding="utf-8")) # revealed: bytes
"abcde".find(123) # error: [invalid-argument-type]
Bytes literals
reveal_type(b"abcde".startswith(b"abc")) # revealed: bool
Method calls on LiteralString
from typing_extensions import LiteralString
def f(s: LiteralString) -> None:
reveal_type(s.find("a")) # revealed: int
Method calls on tuple
def f(t: tuple[int, str]) -> None:
reveal_type(t.index("a")) # revealed: int
Method calls on unions
from typing import Any
class A:
def f(self) -> int:
return 1
class B:
def f(self) -> str:
return "a"
def f(a_or_b: A | B, any_or_a: Any | A):
reveal_type(a_or_b.f) # revealed: <bound method `f` of `A`> | <bound method `f` of `B`>
reveal_type(a_or_b.f()) # revealed: int | str
reveal_type(any_or_a.f) # revealed: Any | <bound method `f` of `A`>
reveal_type(any_or_a.f()) # revealed: Any | int
Method calls on KnownInstance types
[environment]
python-version = "3.12"
type IntOrStr = int | str
reveal_type(IntOrStr.__or__) # revealed: <bound method `__or__` of `typing.TypeAliasType`>