Reputation: 7867
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
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 expectthis
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
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