joel
joel

Reputation: 7867

What is the static type of self?

I want to constrain a method parameter to be of the same type as the class it's called on (see the end for an example). While trying to do that, I've come across this behaviour that I'm struggling to get my head around.

The following doesn't type check

class A:
    def foo(self) -> None:
        pass

A.foo(1)

with

error: Argument 1 to "foo" of "A" has incompatible type "int"; expected "A"

as I'd expect, since I'd have thought A.foo should only take an A. If however I add a self type

from typing import TypeVar

Self = TypeVar("Self")

class A:
    def foo(self: Self) -> None:
        pass

A.foo(1)

it does type check. I would have expected it to fail, telling me I need to pass an A not an int. This suggests to me that the type checker usually infers the type A for self, and adding a Self type overrides that, I'm guessing to object. This fits with the error

from typing import TypeVar

Self = TypeVar("Self")

class A:
    def bar(self) -> int:
        return 0

    def foo(self: Self) -> None:
        self.bar()

error: "Self" has no attribute "bar"

which I can fix if I bound as Self = TypeVar("Self", bound='A')

Am I right that this means self is not constrained, in e.g. the same way I'd expect this to be constrained in Scala?

I guess this only has an impact if I specify the type of self to be anything but the class it's defined on, intentionally or otherwise. I'm also interested to know what the impact is of overriding self to be another type, and indeed whether it even makes sense with how Python resolves and calls methods.

Context

I want to do things like

class A:
    def foo(self: Self, bar: List[Self]) -> Self:
        ...

but I was expecting Self to be constrained to be an A, and was surprised that it wasn't.

Upvotes: 0

Views: 928

Answers (2)

Michael0x2a
Michael0x2a

Reputation: 64058

If you omit a type hint on self, the type checker will automatically assume it has whatever the type of the containing class is.

This means that:

class A:
    def foo(self) -> None: pass

...is equivalent to doing:

class A:
    def foo(self: A) -> None: pass

If you want self to be something else, you should set a custom type hint.


Regarding this code snippet:

from typing import TypeVar

Self = TypeVar("Self")

class A:
    def foo(self: Self) -> None:
        pass

A.foo(1)

Using a TypeVar only once in a function signature is either malformed or redundant, depending on your perspective.

But this is kind of unrelated to the main thrust of your question. We can repair your code snippet by instead doing:

from typing import TypeVar

Self = TypeVar("Self")

class A:
    def foo(self: Self) -> Self:
        return self

A.foo(1)

...which exhibits the same behaviors you noticed.

But regardless of which of the two code snippets we look at, I believe the type checker will indeed assume self has the same type as whatever the upper bound of Self is while type checking the body of foo. In this case, the upper bound is object, as you suspected.

We get this behavior whether or not we're doing anything fancy with self or not. For example, we'd get the exact same behavior by just doing:

def foo(x: Self) -> Self:
    return x

...and so forth. From the perspective of the type checker, there's nothing special about the self parameter, except that we set a default type for it if it's missing a type hint instead of just using Any.


error: "Self" has no attribute "bar"

which I can fix if I bound as Self = TypeVar("Self", bound='A')

Am I right that this means self is not constrained, in e.g. the same way I'd expect this to be constrained in Scala?

I'm unfamiliar with how this is constrained in Scala, but it is indeed the case that if you chose to override the default type of self, you are responsible for setting your own constraints and bounds as appropriate.

To put it another way, once a TypeVar is defined, its meaning won't be changed when you try using it in a function definition. This is the rule for TypeVars/functions in general. And since mostly there's nothing special about self, the same rule also applies there.

(Though type checkers such as mypy will also try doing some basic sanity checks on whatever constraints you end up picking to ensure you don't end up with a method that's impossible to call or whatever. For example, it complain if you tried setting the bound of Self to int.)


Note that doing things like:

from typing import TypeVar, List

Self = TypeVar('Self', bound='A')

class A:
    def foo(self: Self, bar: List[Self]) -> Self:
        ...

class B(A): pass

x = A().foo([A(), A()])
y = B().foo([B(), B()])

reveal_type(x)  # Revealed type is 'A'
reveal_type(y)  # Revealed type is 'B'

...is explicitly supported by PEP 484. The mypy docs also have a few examples.

Upvotes: 1

ShapeOfMatter
ShapeOfMatter

Reputation: 1011

Two things:

self is only half-magic.

The self arg has the magical property that, if you call an attribute of an object as a function, and that function has self as its first arg, then the object itself will be prepended to the explicit args as the self.

I guess any good static analyzer would take as implicit that self has the class in question as its type, which is what you're seeing in your first example.

TypeVar is for polymorphism.

And I think that's what you're trying to do? In your third example, Self can be any type, depending on context. In the context of A.foo(1), Self is int, so self.bar() fails.

It may be possible to write an instance method that can be called as a static method against class non-members with parametric type restrictions, but it's probably not a good idea for any application in the wild. Just name the variable something else and declare the method to be static.

Upvotes: 1

Related Questions