Protocol doesn't seem to agree with SQLAlchemy Mapped attributes
#11545
|
My app has a database and a Swagger API, so I have two models for the same kind of thing: # sqlalchemy model
class Host(DeclarativeMeta):
hostname: Mapped[str]
# pydantic model
class GetHost(BaseModel):
hostname: strElsewhere, I have a bit of code that wants to access that class HasHostname(Protocol):
hostname: str
def process_hostname(obj: HasHostname): ...And when I go to use it with the SQLAlchemy model, I get an error: I'm pretty sure Pyright is technically correct: Is there a way I can persuade Pyright to accept these as compatible, anyway? I mean, other than using |
Replies: 1 comment 3 replies
|
You're right that the diagnostic is technically accurate, but the reason it fires is more subtle than " What's actually happening
reveal_type(Host().hostname) # str
reveal_type(Host.hostname) # Mapped[str]But during protocol matching, pyright does not apply the descriptor. It compares the member's declared type ( This is also why switching to a property didn't help — the source side is still evaluated as A workaround that actually type-checks Make the protocol member read-only (so it's covariant, not invariant) and widen its type to a common supertype of both class HasHostname(Protocol):
@property
def hostname(self) -> str | Mapped[str]: ... # read-only => covariantBoth your models now satisfy it: process_hostname(Host()) # ok (SQLAlchemy, Mapped[str])
process_hostname(GetHost()) # ok (pydantic, str)The trade-off is that inside If you'd rather keep If you think pyright should resolve the descriptor during protocol matching (as it does for direct access, and as mypy does), that'd be worth filing as a separate issue. |
You're right that the diagnostic is technically accurate, but the reason it fires is more subtle than "
Mappedisn't astr," and it explains why none of your workarounds helped.What's actually happening
Mappedis a descriptor. When you access it on an instance, pyright correctly resolves__get__:But during protocol matching, pyright does not apply the descriptor. It compares the member's declared type (
Mapped[str]) against the protocol member, not the__get__-resolved type (str). Combined with the invariance rule for mutable protocol attributes,Mapped[str]has to equalstr, and it doesn't.This is also why s…