Skip to main content
Classes in Sifr look and behave like Python classes, with one important difference: every field must have a type annotation, and the compiler verifies those types at compile time. You get the familiar class, def __init__, and self.field syntax with compile-time safety and zero runtime overhead.

Defining a Class

Declare fields as class-level annotations, then implement __init__ to accept and assign them. The compiler enforces that every field is initialized and that the types match throughout the class body.
class Point:
    x: float
    y: float

    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def distance(self, other: Point) -> float:
        dx: float = self.x - other.x
        dy: float = self.y - other.y
        return (dx * dx + dy * dy) ** 0.5
Instantiate a class by calling it like a function. Field access uses standard dot notation.
def main():
    origin: Point = Point(0.0, 0.0)
    target: Point = Point(3.0, 4.0)
    d: float = origin.distance(target)
    print(f"Distance from origin to (3,4): {d}")

Multiple Methods

A class can have as many methods as you need. Each method receives self as its first parameter, and the compiler enforces that the return type matches the annotation.
class Rectangle:
    width: float
    height: float

    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

    def perimeter(self) -> float:
        return 2.0 * (self.width + self.height)

def main():
    rect: Rectangle = Rectangle(5.0, 3.0)
    print(f"Rectangle area: {rect.area()}")
    print(f"Rectangle perimeter: {rect.perimeter()}")
    print(f"Rectangle width: {rect.width}")
    print(f"Rectangle height: {rect.height}")

Classes in Union Types

Classes are first-class members of union types. Declare a parameter as Dog | Cat | Bird and the compiler tracks all three possibilities throughout the function body.
class Circle:
    radius: float

    def __init__(self, radius: float):
        self.radius = radius

class Square:
    side: float

    def __init__(self, side: float):
        self.side = side

def describe_shape(shape: Circle | Square):
    if isinstance(shape, Circle):
        print(f"Circle: radius={shape.radius}")
    else:
        print(f"Square: side={shape.side}")

def main():
    c: Circle = Circle(10.0)
    s: Square = Square(7.0)
    describe_shape(c)
    describe_shape(s)
After an isinstance(shape, Circle) check, the compiler narrows shape to Circle inside the if block. In the else block it knows shape must be Square. You never need a cast.

isinstance Narrowing with Multi-Member Unions

When your union has more than two members, use elif chains. The compiler eliminates one variant per branch, leaving only the remaining possibilities.
class Dog:
    name: str
    breed: str

class Cat:
    name: str
    color: str

class Bird:
    name: str
    wingspan: float

def describe_pet(pet: Dog | Cat | Bird) -> str:
    if isinstance(pet, Dog):
        return f"{pet.name} is a {pet.breed}"
    elif isinstance(pet, Cat):
        return f"{pet.name} is {pet.color}"
    else:
        return f"{pet.name} has wingspan {pet.wingspan}"

Hashable Classes

Classes are hashable by default. Call hash() on any instance to get a deterministic integer hash based on its field values.
class Color:
    r: int
    g: int
    b: int

    def __init__(self, r: int, g: int, b: int):
        self.r = r
        self.g = g
        self.b = b

def main():
    red: Color = Color(255, 0, 0)
    also_red: Color = Color(255, 0, 0)
    h1: int = hash(red)
    h2: int = hash(also_red)
    print(f"Same color same hash: {h1 == h2}")  # True

Error Subclasses

Subclass Error to define custom error types for use with Result[T, E]. Error subclasses carry typed fields just like any other class.
class ValidationError(Error):
    message: str

def validate_range(x: int, lo: int, hi: int) -> Result[int, ValidationError]:
    if x < lo:
        raise ValidationError(f"value out of range: {x}")
    if x > hi:
        raise ValidationError(f"value out of range: {x}")
    return x
Error subclasses carry no hidden stack trace or exception machinery — only the fields you declare. They are lightweight typed values, not exception objects.
You consume them with try/except at the call site:
def main():
    try:
        v: int = validate_range(50, 0, 100)
        print(f"validated: {v}")
    except ValidationError as e:
        print(f"caught: {e.message}")

    try:
        v2: int = validate_range(-5, 0, 100)
        print(f"validated: {v2}")
    except ValidationError as e:
        print(f"caught: {e.message}")

Class Design Guidelines

Keep classes small and focused on a single concept. Prefer multiple small classes in a union over a single large class with many optional fields.
Sifr does not support multiple inheritance. A class may subclass at most one base class, and only Error is a recognized base type at this time.