-
Notifications
You must be signed in to change notification settings - Fork 288
Proposal: Trait for strict, non-widening type bounds (solving LSP issues in binary ops)
#2143
-
Abstract
I propose introducing a new construct named Trait. Unlike an ABC or Protocol, a Trait acts as a strict constraint that prevents the type solver from "widening" a TypeVar to the base type. This specifically solves the long-standing issue of defining binary operations (like Semigroups) on subclasses without violating the Liskov Substitution Principle (LSP).
The Problem: The "Binary Operation" Paradox
In current Python typing, if we want to define a generic binary operation, we often run into a conflict between type safety and LSP.
from abc import ABC, abstractmethod from typing import Self class Semigroup(ABC): @abstractmethod def op(self, other: Self) -> Self: ... class IntSemigroup(Semigroup): def __init__(self, value: int) -> None: self.value = value def op(self, other: "IntSemigroup") -> "IntSemigroup": # ⚠️ This is technically an LSP violation. # Semigroup.op promised to accept ANY Semigroup, # but IntSemigroup only accepts IntSemigroup. return IntSemigroup(self.value + other.value) class StringSemigroup(Semigroup): def __init__(self, value: str) -> None: self.value = value def op(self, other: "StringSemigroup") -> "StringSemigroup": return StringSemigroup(self.value + other.value) def combine[T: Semigroup](a: T, b: T) -> T: return a.op(b) # ❌ The Runtime Failure combine(IntSemigroup(1), StringSemigroup("2"))
Why this fails today:
- Widen-to-Bound: When
combineis called with anIntand aString, the type checker sees both satisfySemigroup. It resolvesTtoSemigroup. - LSP Conflict: To be type-safe,
Semigroup.opmust accept any otherSemigroup. But for concrete logic (like addition), it must only accept its own type.
The Solution: Trait
If we define Semigroup as a Trait, we change the rules of engagement. A Trait is not a "parent type" that objects can be widened to; it is a requirement that a concrete type must satisfy.
from typing import Trait # Proposed new construct class Semigroup(Trait): @abstractmethod def op(self, other: Self) -> Self: ... def combine[T: Semigroup](a: T, b: T) -> T: return a.op(b) # HOW IT WORKS: # 1. T can NEVER resolve to "Semigroup" itself. It must be a concrete leaf type. # 2. Since T must be a single concrete type, the solver attempts to unify # IntSemigroup and StringSemigroup. # 3. Unification fails because they are different concrete types. # ✅ Type Checker Error: # "Cannot unify 'IntSemigroup' and 'StringSemigroup' for strict Trait-bound 'T'" combine(IntSemigroup(1), StringSemigroup("2"))
Why this is better
- LSP Preservation: Since
Semigroupis aTraitand not a base class, we are not saying "Int is a Semigroup" in a way that requires substitution. We are saying "Int implements the Semigroup interface." - No More Runtime
TypeError: The type checker catches "mixed implementation" calls that currently pass when usingABCbounds. - Rust-like Traits: It brings the safety of Rust traits or Haskell typeclasses to Python’s generic functions.
Proposed Semantics
- Non-Reifiable: You cannot instantiate a
Trait, and aTypeVarbound by aTraitcannot resolve to thatTrait. - Strict Unification: When a
TypeVaris bound by aTrait, all arguments associated with thatTypeVarmust unify to the exact same concrete type.
Note: "Protocols also suffer from the widening issue. If I have
def f[T: MyProtocol](a: T, b: T), the checker will still happily resolveTto the Protocol itself if I pass two different objects that both happen to implement it. I am looking for a way to force concrete type unification."
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 5
Replies: 1 comment
-
Since the new thing being expressed here is some additional constraints on typevar solving, I think it would be preferable if the new type system concept introduced were more narrowly targeted to typevar solving -- something more like https://discuss.python.org/t/support-function-with-multiple-generic-parameters-same-type/57931/6
Here it feels like we are introducing yet another variant of Protocols or ABCs (a type system feature with large surface area), just in order to (effectively) tweak the semantics of a generic protocol.
Beta Was this translation helpful? Give feedback.