I'm trying to understand how type variance works with Python protocols and generics. My test cases seem to contradict what I expect regarding invariant, covariant, and contravariant behavior.
Here's a minimal example demonstrating the issue:
from typing import TypeVar, Protocol
# Type variables
T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
# Class hierarchy
class Animal: pass
class Dog(Animal): pass
# Protocols
class Feeder(Protocol[T]):
def feed(self, animal: T) -> T: ...
class Adopter(Protocol[T_co]):
def adopt(self) -> T_co: ...
class Walker(Protocol[T_contra]):
def walk(self, animal: T_contra) -> None: ...
# Implementations
class AnimalFeeder:
def feed(self, animal: Animal) -> Animal: ...
class DogFeeder:
def feed(self, animal: Dog) -> Dog: ...
class AnimalAdopter:
def adopt(self) -> Animal: ...
class DogAdopter:
def adopt(self) -> Dog: ...
class AnimalWalker:
def walk(self, animal: Animal) -> None: ...
class DogWalker:
def walk(self, animal: Dog) -> None: ...
When testing type assignments, some cases behave differently than expected:
# Test cases with expected vs actual behavior
feeder1: Feeder[Dog] = DogFeeder() # Expected ✅ Actual ✅ (exact match)
feeder2: Feeder[Dog] = AnimalFeeder() # Expected ❌ Actual ❌ (invariant)
feeder3: Feeder[Animal] = DogFeeder() # Expected ❌ Actual ✅ (Why does this work?)
adopter1: Adopter[Dog] = DogAdopter() # Expected ✅ Actual ✅ (exact match)
adopter2: Adopter[Dog] = AnimalAdopter() # Expected ❌ Actual ❌ (return type mismatch)
adopter3: Adopter[Animal] = DogAdopter() # Expected ✅ Actual ✅ (covariant, correct)
walker1: Walker[Dog] = DogWalker() # Expected ✅ Actual ✅ (exact match)
walker2: Walker[Dog] = AnimalWalker() # Expected ✅ Actual ❌ (Should work with contravariance?)
walker3: Walker[Animal] = DogWalker() # Expected ❌ Actual ✅ (Why does this work?)
Questions:
I'm using Python 3.10 and PyCharm 2024.3.1.
feeder3 works because Python's structural typing checks method return type covariance (accepting Dog as Animal), but incorrectly ignores parameter contravariance (should reject Dog input for Animal parameter). This is a type checker limitation (PyCharm/mypy may differ).
walker2 fails due to insufficient contravariance support in some type checkers. AnimalWalker (handles supertype Animal) should satisfy Walker[Dog] (needs subtype input) via contravariance, but PyCharm doesn't recognize
Behaviors are partially incorrect - true variance rules (per PEP 544) would reject feeder3 and accept walker2. PyCharm's checker has limitations in enforcing variance for protocols, unlike stricter tools like mypy.