Reputation: 4572
Is it possible to type hint an additional attribute of an attribute of a class?
I have a class attribute Outer.inner
that can be any instance of a class that conforms to a protocol InnerProto
.
#### I do not control this code #######
from typing import Protocol
class InnerProto(Protocol):
arg: int
class OuterProto(Protocol):
inner: InnerProto
Inner
class conforms to the protocol InnerProto
, but it also has an additional attribute (which as far as I understand doesn't violate protocol conformity)
class Outer:
def __init__(self, inner: InnerProto):
self.inner: InnerProto = inner
class Inner:
def __init__(self, arg: int):
self.arg: int = arg # this is part of the protocol
self.arg2: int = arg # this is not
Pycharm (v2021.2) inspection highlights the attribute arg2
as an unresolved attribute.
def test_type_hint_example():
obj = Outer(Inner(1))
assert obj.inner.arg == 1
assert obj.inner.arg2 == 1 # inspection highlights arg2 as "unresolved attribute"
Is it possible to typehint so that inspection recognizes what arg2 is?
Normally I would be inclined to alter the type hint of Outer.inner
but in this case that would be a mistake. inner
can be ANY class that conforms to InnerProto
.
My second thought was to override the type-hint for the attribute of the instance. This type of trick has worked non-attribute objects, but in this case it results in a different inspection error: "Non-self attribute could not be type hinted"
def test_option1():
obj = Outer(Inner(1))
obj.inner: Inner # fixes inspection for arg2, but "Non-self attribute could not be type hinted" occurs
assert obj.inner.arg == 1
assert obj.inner.arg2 == 1
What other options are there?
Upvotes: 0
Views: 1281
Reputation: 7539
I think the issue here is that the code isn't type-safe.
Your Outer.__init__
function is:
class Outer:
def __init__(self, inner: InnerProto):
self.inner = inner
# <-- snip -->
You've annotated the inner
argument with InnerProto
. That means that the Outer.inner
attribute could be of any type, as long as that type conforms to the InnerProto
protocol.
According to you, this is the correct type-hint:
Normally I would be inclined to alter the type hint of
Outer.inner
but in this case that would be a mistake.inner
can be ANY class that conforms toInnerProto
.
If this is the case, then there is no way for a static type-checker to verify that Outer.inner
will have any attributes or methods that are not specified in InnerProto
. This is why the type-checker is raising an error in your test_type_hint_example
function: you've told the type-checker that Outer.inner
could be of any type as long as that type conforms to InnerProto
, but InnerProto
says nothing about an arg2
attribute, and not all python types have an arg2
attribute, so therefore there's a possibility that you'll get an AttribiteError
when you access Outer.inner.arg2
.
If you are certain that, in the context of this specific function, Outer.inner
will be of type Inner
, and therefore can be guaranteed to have an arg2
attribute — despite the fact that in most situations, Outer.inner
"could be ANY class that conforms to InnerProto
" — then you could use typing.cast
like this:
from typing import cast
def test_type_hint():
obj = Outer(Inner(1))
inner = cast(Inner, obj.inner)
assert inner.arg == 1
assert inner.arg2 == 1
In reality, however, if you are certain that Outer.inner
can be guaranteed to have an arg2
attribute, that most likely indicates that you do, in fact, have the wrong type hint for Outer.inner
(it's hard to tell you what the right type hint is without seeing more of your code base). Using typing.cast
in the way I've just demonstrated above should generally only be used as a last resort — it's more a method of evading the type-checker, which won't help you in the long run if your code isn't type-safe.
Upvotes: 1