Skip to content

Classes

py4 classes are value-based records with methods. They are closer to C structs with associated functions than to Python OOP.

Basic class

class Point:
x: int
y: int
def sum(self: Point) -> int:
return self.x + self.y
def main() -> None:
p: Point = Point(3, 4)
p.x = 10
print(p.x)
print(p.sum())
print(p)

Instantiate with positional arguments matching the field order. The self parameter is always explicitly typed with the class name.

Methods

class Segment:
start: Point
end: Point
def span(self: Segment) -> int:
return (self.end.x - self.start.x) + (self.end.y - self.start.y)
def main() -> None:
s = Segment(Point(1, 2), Point(5, 9))
print(s.span())

Methods lower to plain C functions. Dispatch is static — there is no vtable.

__init__

When you need custom initialization logic, define __init__. The self parameter is typed here too:

class Counter:
value: int
label: str
def __init__(self: Counter, start: int, name: str) -> None:
self.value = start
self.label = name
def bump(self: Counter, delta: int) -> int:
return self.value + delta
def main() -> None:
c: Counter = Counter(4, "hits")
print(c.value)
print(c.label)
print(c.bump(3))
print(c)

__init__ cannot be called directly as a method. It lowers to a C initializer called by the generated constructor.

Inheritance

Single inheritance is supported:

class Animal:
name: str
age: int
def label(self: Animal) -> str:
return self.name
class Dog(Animal):
bark: int
def total(self: Dog) -> int:
return self.age + self.bark
class LoudDog(Dog):
def twice(self: LoudDog) -> int:
return self.total() + self.total()
def main() -> None:
dog = Dog("rex", 4, 3)
loud = LoudDog("max", 2, 5)
print(dog.name)
print(dog.label())
print(dog.total())
print(loud.label())
print(loud.twice())
print(loud)
  • Inherited fields are flattened into the subclass layout.
  • Inherited methods are accessible on the subclass.

super()

super() is supported for parent method calls and parent __init__ calls within subclass methods:

class Animal:
name: str
def __init__(self: Animal, name: str) -> None:
self.name = name
def label(self: Animal) -> str:
return self.name
class Dog(Animal):
bark: int
def __init__(self: Dog, name: str, bark: int) -> None:
super().__init__(name)
self.bark = bark
def label(self: Dog) -> str:
return super().label()
def total(self: Dog) -> int:
return self.bark + len(super().label())
def main() -> None:
dog = Dog("rex", 4)
print(dog.label())
print(dog.total())
print(dog)

super() is valid only inside subclass methods and only for method-call syntax.

Private members

Fields and methods prefixed with __ are private to the class:

class Counter:
__value: int
label: str
def __bump(self: Counter) -> int:
self.__value = self.__value + 1
return self.__value
def reveal(self: Counter) -> int:
return self.__bump()
def main() -> None:
c: Counter = Counter(4, "hits")
print(c.reveal())
print(c.label)

External access to __value or __bump is rejected at compile time.

Value semantics

Classes are value types. Assignment copies the struct. Managed fields (lists, dicts) inside a class share underlying refcounted storage:

class Bucket:
values: list[int]
def main() -> None:
original: Bucket = Bucket([1, 2])
alias: Bucket = original
alias.values.append(3) # shared list — both see the change
print(original.values) # [1, 2, 3]
alias.values = [9] # reassigning breaks the alias
print(original.values) # [1, 2, 3]
print(alias.values) # [9]

Limitations

  • No dynamic attributes
  • No __str__, __repr__, or operator overloading
  • No multiple inheritance or MRO
  • super field access is not supported, only method calls
  • Method dispatch is always static