Newtypes¶
Newtypes declare a distinct nominal type around one underlying value:
type UserId = newtype int
Construct a newtype explicitly with one positional underlying value:
user_id = UserId(42)
The wrapped value is available as .0.
Trait Adoption¶
Newtypes can adopt traits with the same with TraitName clause used by models, classes, and enums:
trait ToInt:
def convert(self) -> int: ...
type UserId = newtype int with ToInt:
def convert(self) -> int:
return self.0
When two adopted traits require the same method name, target each method implementation with for TraitName before the return arrow:
trait ToInt:
def convert(self) -> int: ...
trait ToStr:
def convert(self) -> str: ...
type UserId = newtype int with ToInt, ToStr:
def convert(self) for ToInt -> int:
return self.0
def convert(self) for ToStr -> str:
return str(self.0)
The for ToInt qualifier belongs to the method declaration. It says which adopted trait the method satisfies; it does not modify the return type.
Validated Construction¶
A newtype may define the canonical validation hook from_underlying:
type Attempts = newtype int:
def from_underlying(n: int) -> Result[Self, ValidationError]:
if n <= 0:
return Err(ValidationError("attempts must be >= 1"))
return Ok(Attempts(n))
The hook must be a static method with exactly one ordinary parameter whose type is the newtype's underlying type. Its return type must be Result[T, ValidationError] or Result[Self, ValidationError].
ValidationError("message") creates the canonical validation error. Use ValidationError(message="...", code="...") when a stable error code is useful.
Implicit Sites¶
The compiler inserts validated coercion only where the destination type is already explicit:
- Function and method arguments.
- Typed local initializers.
- Static initializers.
- Model and class constructor fields.
- Explicit
T(value)construction.
Implicit coercion does not parse unrelated primitive types. A str does not become an int on the way into an int-backed newtype.
Reassignment is not an implicit coercion site:
type Attempts = newtype int
def main() -> None:
mut attempts: Attempts = Attempts(1)
# attempts = 2 # type error
Constraints¶
Primitive integer and float underlyings may use numeric constraints:
type PositiveInt = newtype int[gt=0]
type Percentage = newtype int[ge=0, le=100]
Supported constraint keys are gt, ge, lt, and le. Generated constraint checks use the same validated construction sites as from_underlying.
Aggregate Validation¶
Model and class constructors aggregate validated field errors before raising:
type PositiveInt = newtype int[gt=0]
model Bounds:
low: PositiveInt
high: PositiveInt
def main() -> None:
bounds = Bounds(low=1, high=2)
If more than one validated field fails, the raised validation error includes the constructor target and each failed field.
Opting Out¶
Use @no_implicit_coercion when callers must construct the newtype explicitly:
@no_implicit_coercion
type Attempts = newtype int
Explicit Attempts(value) construction remains available.