Files
ruff/crates/red_knot_python_semantic/resources/mdtest/call/methods.md
2025-02-20 17:27:41 +01:00

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.f is equivalent to getattr_static(C, "f").__get__(None, C)
  • C().f is equivalent to getattr_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`>