ruff/crates/ty_python_semantic/resources/mdtest/class/super.md
2025-05-03 19:49:15 +02:00

12 KiB
Raw Blame History

Super

Python defines the terms bound super object and unbound super object.

An unbound super object is created when super is called with only one argument. (e.g. super(A)). This object may later be bound using the super.__get__ method. However, this form is rarely used in practice.

A bound super object is created either by calling super(pivot_class, owner) or by using the implicit form super(), where both the pivot class and the owner are inferred. This is the most common usage.

Basic Usage

Explicit Super Object

super(pivot_class, owner) performs attribute lookup along the MRO, starting immediately after the specified pivot class.

class A:
    def a(self): ...
    aa: int = 1

class B(A):
    def b(self): ...
    bb: int = 2

class C(B):
    def c(self): ...
    cc: int = 3

reveal_type(C.__mro__)  # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]

super(C, C()).a
super(C, C()).b
# error: [unresolved-attribute] "Type `<super: Literal[C], C>` has no attribute `c`"
super(C, C()).c

super(B, C()).a
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `b`"
super(B, C()).b
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `c`"
super(B, C()).c

# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `a`"
super(A, C()).a
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `b`"
super(A, C()).b
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `c`"
super(A, C()).c

reveal_type(super(C, C()).a)  # revealed: bound method C.a() -> Unknown
reveal_type(super(C, C()).b)  # revealed: bound method C.b() -> Unknown
reveal_type(super(C, C()).aa)  # revealed: int
reveal_type(super(C, C()).bb)  # revealed: int

Implicit Super Object

The implicit form super() is same as super(__class__, <first argument>). The __class__ refers to the class that contains the function where super() is used. The first argument refers to the current methods first parameter (typically self or cls).

from __future__ import annotations

class A:
    def __init__(self, a: int): ...
    @classmethod
    def f(cls): ...

class B(A):
    def __init__(self, a: int):
        # TODO: Once `Self` is supported, this should be `<super: Literal[B], B>`
        reveal_type(super())  # revealed: <super: Literal[B], Unknown>
        super().__init__(a)

    @classmethod
    def f(cls):
        # TODO: Once `Self` is supported, this should be `<super: Literal[B], Literal[B]>`
        reveal_type(super())  # revealed: <super: Literal[B], Unknown>
        super().f()

super(B, B(42)).__init__(42)
super(B, B).f()

Unbound Super Object

Calling super(cls) without a second argument returns an unbound super object. This is treated as a plain super instance and does not support name lookup via the MRO.

class A:
    a: int = 42

class B(A): ...

reveal_type(super(B))  # revealed: super

# error: [unresolved-attribute] "Type `super` has no attribute `a`"
super(B).a

Attribute Assignment

super() objects do not allow attribute assignment — even if the attribute is resolved successfully.

class A:
    a: int = 3

class B(A): ...

reveal_type(super(B, B()).a)  # revealed: int
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `<super: Literal[B], B>`"
super(B, B()).a = 3
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `super`"
super(B).a = 5

Dynamic Types

If any of the arguments is dynamic, we cannot determine the MRO to traverse. When accessing a member, it should effectively behave like a dynamic type.

class A:
    a: int = 1

def f(x):
    reveal_type(x)  # revealed: Unknown

    reveal_type(super(x, x))  # revealed: <super: Unknown, Unknown>
    reveal_type(super(A, x))  # revealed: <super: Literal[A], Unknown>
    reveal_type(super(x, A()))  # revealed: <super: Unknown, A>

    reveal_type(super(x, x).a)  # revealed: Unknown
    reveal_type(super(A, x).a)  # revealed: Unknown
    reveal_type(super(x, A()).a)  # revealed: Unknown

Implicit super() in Complex Structure

from __future__ import annotations

class A:
    def test(self):
        reveal_type(super())  # revealed: <super: Literal[A], Unknown>

    class B:
        def test(self):
            reveal_type(super())  # revealed: <super: Literal[B], Unknown>

            class C(A.B):
                def test(self):
                    reveal_type(super())  # revealed: <super: Literal[C], Unknown>

            def inner(t: C):
                reveal_type(super())  # revealed: <super: Literal[B], C>
            lambda x: reveal_type(super())  # revealed: <super: Literal[B], Unknown>

Built-ins and Literals

reveal_type(super(bool, True))  # revealed: <super: Literal[bool], bool>
reveal_type(super(bool, bool()))  # revealed: <super: Literal[bool], bool>
reveal_type(super(int, bool()))  # revealed: <super: Literal[int], bool>
reveal_type(super(int, 3))  # revealed: <super: Literal[int], int>
reveal_type(super(str, ""))  # revealed: <super: Literal[str], str>

Descriptor Behavior with Super

Accessing attributes through super still invokes descriptor protocol. However, the behavior can differ depending on whether the second argument to super is a class or an instance.

class A:
    def a1(self): ...
    @classmethod
    def a2(cls): ...

class B(A): ...

# A.__dict__["a1"].__get__(B(), B)
reveal_type(super(B, B()).a1)  # revealed: bound method B.a1() -> Unknown
# A.__dict__["a2"].__get__(B(), B)
reveal_type(super(B, B()).a2)  # revealed: bound method type[B].a2() -> Unknown

# A.__dict__["a1"].__get__(None, B)
reveal_type(super(B, B).a1)  # revealed: def a1(self) -> Unknown
# A.__dict__["a2"].__get__(None, B)
reveal_type(super(B, B).a2)  # revealed: bound method Literal[B].a2() -> Unknown

Union of Supers

When the owner is a union type, super() is built separately for each branch, and the resulting super objects are combined into a union.

class A: ...

class B:
    b: int = 42

class C(A, B): ...
class D(B, A): ...

def f(x: C | D):
    reveal_type(C.__mro__)  # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
    reveal_type(D.__mro__)  # revealed: tuple[Literal[D], Literal[B], Literal[A], Literal[object]]

    s = super(A, x)
    reveal_type(s)  # revealed: <super: Literal[A], C> | <super: Literal[A], D>

    # error: [possibly-unbound-attribute] "Attribute `b` on type `<super: Literal[A], C> | <super: Literal[A], D>` is possibly unbound"
    s.b

def f(flag: bool):
    x = str() if flag else str("hello")
    reveal_type(x)  # revealed: Literal["", "hello"]
    reveal_type(super(str, x))  # revealed: <super: Literal[str], str>

def f(x: int | str):
    # error: [invalid-super-argument] "`str` is not an instance or subclass of `Literal[int]` in `super(Literal[int], str)` call"
    super(int, x)

Even when super() is constructed separately for each branch of a union, it should behave correctly in all cases.

def f(flag: bool):
    if flag:
        class A:
            x = 1
            y: int = 1

            a: str = "hello"

        class B(A): ...
        s = super(B, B())
    else:
        class C:
            x = 2
            y: int | str = "test"

        class D(C): ...
        s = super(D, D())

    reveal_type(s)  # revealed: <super: Literal[B], B> | <super: Literal[D], D>

    reveal_type(s.x)  # revealed: Unknown | Literal[1, 2]
    reveal_type(s.y)  # revealed: int | str

    # error: [possibly-unbound-attribute] "Attribute `a` on type `<super: Literal[B], B> | <super: Literal[D], D>` is possibly unbound"
    reveal_type(s.a)  # revealed: str

Supers with Generic Classes

[environment]
python-version = "3.12"
from ty_extensions import TypeOf, static_assert, is_subtype_of

class A[T]:
    def f(self, a: T) -> T:
        return a

class B[T](A[T]):
    def f(self, b: T) -> T:
        return super().f(b)

Invalid Usages

Unresolvable super() Calls

If an appropriate class and argument cannot be found, a runtime error will occur.

from __future__ import annotations

# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
reveal_type(super())  # revealed: Unknown

def f():
    # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
    super()

# No first argument in its scope
class A:
    # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
    s = super()

    def f(self):
        def g():
            # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
            super()
        # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
        lambda: super()

        # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
        (super() for _ in range(10))

    @staticmethod
    def h():
        # error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
        super()

Failing Condition Checks

[environment]
python-version = "3.12"

super() requires its first argument to be a valid class, and its second argument to be either an instance or a subclass of the first. If either condition is violated, a TypeError is raised at runtime.

def f(x: int):
    # error: [invalid-super-argument] "`int` is not a valid class"
    super(x, x)

    type IntAlias = int
    # error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class"
    super(IntAlias, 0)

# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[""])` call"
# revealed: Unknown
reveal_type(super(int, str()))

# error: [invalid-super-argument] "`Literal[str]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[str])` call"
# revealed: Unknown
reveal_type(super(int, str))

class A: ...
class B(A): ...

# error: [invalid-super-argument] "`A` is not an instance or subclass of `Literal[B]` in `super(Literal[B], A)` call"
# revealed: Unknown
reveal_type(super(B, A()))

# error: [invalid-super-argument] "`object` is not an instance or subclass of `Literal[B]` in `super(Literal[B], object)` call"
# revealed: Unknown
reveal_type(super(B, object()))

# error: [invalid-super-argument] "`Literal[A]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[A])` call"
# revealed: Unknown
reveal_type(super(B, A))

# error: [invalid-super-argument] "`Literal[object]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[object])` call"
# revealed: Unknown
reveal_type(super(B, object))

super(object, object()).__class__

Instance Member Access via super

Accessing instance members through super() is not allowed.

from __future__ import annotations

class A:
    def __init__(self, a: int):
        self.a = a

class B(A):
    def __init__(self, a: int):
        super().__init__(a)
        # TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
        super().a

# error: [unresolved-attribute] "Type `<super: Literal[B], B>` has no attribute `a`"
super(B, B(42)).a

Dunder Method Resolution

Dunder methods defined in the owner (from super(pivot_class, owner)) should not affect the super object itself. In other words, super should not be treated as if it inherits attributes of the owner.

class A:
    def __getitem__(self, key: int) -> int:
        return 42

class B(A): ...

reveal_type(A()[0])  # revealed: int
reveal_type(super(B, B()).__getitem__)  # revealed: bound method B.__getitem__(key: int) -> int
# error: [non-subscriptable] "Cannot subscript object of type `<super: Literal[B], B>` with no `__getitem__` method"
super(B, B())[0]