attribute types¶
a type parameter's members can be named in a type position. T.a is the type of
the member a on whatever T turns out to be:
class A1:
a: int
class A2(A1):
a: bool
class B[T: A1]:
x: T.a
def check(b1: B[A1], b2: B[A2]):
reveal_type(b1.x) # int
reveal_type(b2.x) # bool
T is unknown where the annotation is written, so the lookup cannot be
performed there. it is instead kept symbolic — like an
arithmetic operation on a type parameter — and
re-resolved against each specialization, so a subclass that redeclares the
member is honoured rather than flattened to the bound's declaration.
the same works for a function's type parameters:
a specialized receiver¶
the receiver does not have to be a bare type parameter. any type expression works, so a generic can be asked what a member of it becomes at a given specialization:
class X[T: A1]:
x: T
y: T.a
class Z:
p: X[A1].x # A1
q: X[A2].y # bool
r: X[A2].x.a # bool — receivers chain
a receiver that still mentions a type parameter stays symbolic, so it re-resolves at each specialization just as the bare form does:
scope¶
a dotted name is the one receiver shape that keeps its ordinary meaning: it
names the type it resolves to, so mod.Class and Outer.Inner are unaffected
and only a type parameter is read as an attribute type there.
class Outer:
class Inner: ...
x: Outer.Inner # an `Inner` instance, not the type of the member `Inner`
every other receiver shape — a subscript, a chain built on one — has no other meaning in a type position, so there is nothing to collide with.
a parameter pack is not a receiver either: P.args and P.kwargs name the
components of the pack rather than a member of it, and basedpython spells those
components *P and **P instead. nor is an Annotated
metadata element a type position — Annotated[int, T.a] is an attribute access
on the type parameter itself, not an attribute type.
the receiver may be a class or a function type parameter, and the attribute type may appear anywhere a type may: an annotation, a signature, inside another type, or as the value of a generic type alias.
any member works — a field, a method, a property, a nested class, or one inherited from a base of the bound. a member the bound does not have is reported as an unresolved attribute, so an attribute type is checked where it is written and not only where it is specialized:
T: (A1, A2) is a tuple bound, not a constraint list — T is bounded by the
tuple type, which has no a, so T.a there is an unresolved attribute:
a parameter constrained by a type mapping does resolve, per constraint, because each specialization picks one of them:
in a value position the same access unions over the constraints, since the
parameter stands for any one of them: t.a over T in (A1, A2) is
int | bool. the lowered annotation is that union too, which is the widening
every attribute type makes when python cannot spell the dependency.
before it is specialized, an attribute type behaves as the member's type on the parameter's bound — the guarantee every specialization satisfies:
polyfill¶
there is no runtime construct. python cannot express a member type that depends on a type parameter, so the annotation is resolved at transpile time and written out as the member's type on the parameter's bound:
transpiles to:
a member whose type has no python spelling at all — a method, whose type is a
bound method — is written out as Any. the precise type is still enforced
inside the .by file; only the emitted annotation widens. this is the same
trade every symbolic operation makes when it lowers to
its reduced form, and it is not reported: the source is valid, and only the
runtime artifact is less precise than the .by file it came from.