Classes and construction
A class declares typed fields and associated methods. It has no inheritance, dynamic attributes, class variables, properties, or runtime method lookup.
class Point:
x: i64
y: i64
def squared_length(self) -> Result[i64, OverflowError[i64]]:
Ok(self.x * self.x + self.y * self.y)
Without __init__, the generated constructor takes fields in declaration order.
Point(3, 4) and Point.new(3, 4) return Point directly. Construction itself
allocates nothing; arguments and initializer operations retain their own error
contracts. new is a reserved class member.
A class name without arguments is a function value
with the constructor’s positional signature: make = Point lets
make(3, 4) construct a point. A generic class needs an explicit instance,
such as Cell[i64], or a concrete expected Callable result naming the class
instance. Constructor values require all positional arguments; parameter names
and defaults belong to the statically resolved constructor call. Taking the
value allocates nothing and preserves constructor visibility and fallibility.
An explicit __init__ overrides the generated constructor and returns either
() or Result[(), E]. The fallible form makes both constructor spellings
and constructor function values return precisely Result[Class, E]. The declared
error type is independent of the caller’s arithmetic mode; successful fallible
initializers write explicit Ok(()). Non-unit Result success types are rejected.
Storage itself allocates nothing, so construction does not add AllocError.
Initializer operations and parameter defaults must declare or explicitly map
their actual errors into E, including allocation failures when applicable.
__del__ and __exit__ still require (); cleanup cannot propagate failure.
Class parameters in an initializer output, including a parameter naming the
whole Result, are substituted before validating that output. Unit class type
arguments remain unsupported; a concrete unit alias can still name an
initializer output in an otherwise valid generic class.
Fields may contain classes, enums, tuples, collections, and resolved owned
closures, but not references, generators, or unit.
Every successful initializer exit must initialize every field. Branches merge
their definitely initialized fields; a loop alone cannot establish initialization
because it may run zero times. Field assignments performed inside an if or
match block used as a binding or assignment’s value do not establish
initialization after that value block. Assign the selected value directly to
self.field instead; the outer assignment initializes that field. See
conditional values.
Initialized fields may be read. Borrowing or
passing the whole partially initialized instance is rejected, and initialization
cannot be delegated to another method. Constructors accept named arguments,
using field names for generated constructors and parameter names for explicit
initializers. Explicit initializer parameter defaults are inherited by both
constructor spellings. See call rules.
Field defaults, constructor overloading, and static methods are unsupported.
Failed initialization destroys initialized fields and remaining owned arguments and skips the class’s destructor. The destructor becomes active only after successful initialization; see cleanup.
Classes can define arithmetic operator hooks
such as __add__ and __neg__, compiled as direct method calls. Any other
__name__ method besides __init__, __del__, __enter__ and __exit__ is
rejected, so Python
special methods such as __str__ and __len__ cannot be silently ignored.
Class identity is nominal. Structural equality compares fields without calling
user methods. Printing uses Point(x=3, y=4) unless the class defines
display or debug. Comparison and hashing
hooks such as __eq__, __lt__ and __hash__ are rejected; see
equality, ordering and hashing.
Automatic equality and formatting reject recursive data. Instance moves, field access, and explicit
duplication follow ownership; receiver signatures
follow methods, and constructor access follows
visibility.
A class pattern such as
case Point(x, y=0): takes a class apart in match, written like the
generated constructor with every field named once.
One-field classes
A class with exactly one field and no __del__ is represented as that field.
Wrapping a value in such a class, reading its field, or moving the field out
does not copy or allocate, so a wrapper such as class Handle: _file: File
costs the same as the bare File. The field’s type is unrestricted: a scalar,
str, a collection, an enum, or another class. Nothing in the source changes:
the class keeps its name, methods, equality, and printing
(Handle(_file=...)), and the field keeps its
visibility. A one-field class with __del__ keeps a separate
record because its cleanup state needs storage. See
value representations.
A field whose type is another class stores that class’s fields inside the
owner, at a fixed position. Reading path.start.x costs the same as reading a
field of path itself, and moving or returning path copies its bytes once,
with no per-field address work. This is not visible in source: nested fields
still move, borrow, and run __del__ as other fields do.
Copy classes
@copy on the line before class declares a copy class. Its instances copy on
ordinary value use, as copyable tuples and enums do: assignment, owned argument
passing, return, collection insertion, capture, and by-value receivers
(self: Point) all leave the source usable. Copying duplicates the inline
fields and never allocates; string fields share immutable storage. copy(value)
still accepts the instance. Other classes remain affine.
@copy
class Point:
x: i64
y: i64
def main() -> Result[(), IoError]:
p = Point(1, 2)
mut q = p
q.x = 5
print(p, q)?
Ok(())
Point(x=1, y=2) Point(x=5, y=2)
The declaration is checked as a whole:
- Every field must copy: a scalar,
str, another@copyclass, or a tuple or enum whose components copy. The error names the field and its type. - The class cannot define
__del__, because each copy would run it.
A generic @copy class copies when its type arguments copy, as a tuple does.
Pair[i64] and Pair[str] copy; Pair[list[i64]] is accepted and moves. The
field rule is checked at the definition, whether or not the class is used: a
field must copy whenever every type argument copies. T, (T, U), Option[T]
and Pair[T] qualify; a field such as items: list[T] or b: Box[T] moves
for every argument and is reported at its declaration. A
CopyType bound (or IntType or
FloatType) restricts the class to instances that copy:
@copy
class Pair[T]:
left: T
right: T
def main() -> Result[(), AllocError | IoError]:
numbers = Pair(1, 2)
copied = numbers
lists = Pair([1]?, [2]?)
moved = lists
print(numbers, copied, moved)?
Ok(())
Pair[i64](left=1, right=2) Pair[i64](left=1, right=2) Pair[list[i64]](left=[1], right=[2])
Equality, printing, field access, and matching are unchanged. Classes are
collection keys when every field
is a supported key type. Custom __del__ cleanup is allowed, including in
nested fields; such keys cannot be copied for set algebra or key-view to_set().
Hashing reads their
fields structurally, just as equality does. Ordinary instances move into the
collection; @copy instances copy. __hash__ and __eq__ hooks are unsupported.
Annotations
An annotation is @name alone on a line directly before a declaration. The
set is closed and compiler-known; there are no user decorators. The
annotations are @copy, @newtype, and @redacted. @copy and @newtype
apply only to class declarations, including pub class; @redacted also
applies to enums. An unknown name, arguments, a repeated annotation, or an
annotation before any other declaration, member, or statement is an error.
@newtype makes a class forward its field’s operators; see
newtype forwarding. The
class must declare exactly one field and cannot define __init__. It can
have type parameters (@newtype class Wrapper[T]: value: T) and __del__.
@copy and @newtype are independent. @newtype does not imply @copy: a
@newtype class over i64 still moves on assignment unless it is also
@copy. Forwarded operators borrow their operands, so a + a and reusing a
after a + b work either way. A class can carry both annotations, in either
order, with the same meaning:
@newtype
@copy
class Seconds:
value: f64
def main() -> Result[(), IoError]:
a = Seconds(1.5)
b = a
print(a + b)?
Ok(())
Seconds(value=3.0)
Neither annotation changes layout or printing. Every class with one field and
no __del__ uses the one-field representation, with or
without @newtype, and a @newtype value prints as its class
(Seconds(value=3.0)). Annotation names follow
@ and do not reserve words: copy remains an ordinary name, and the builtin
copy(value) is unaffected.
Redacted types
@redacted on a class or enum hides its contents from every automatic
formatting path: print, str.repr, f-string fields, and the report main
writes for a returned error. The value renders as its type name followed by
(@redacted), at any nesting depth: in a list, dictionary, tuple, Option,
Result, enum payload, or another class’s field.
@redacted
class ApiKey:
secret: str
class Client:
host: str
key: ApiKey
def main() -> Result[(), AllocError | IoError]:
client = Client("example.com", ApiKey("hunter2"))
print(client)?
print([ApiKey("other")]?)?
print(client.key.secret.byte_len())?
Ok(())
Client(host="example.com", key=ApiKey(@redacted))
[ApiKey(@redacted)]
7
The rules:
- Redaction applies to the whole value. There is no per-field form; a class
that mixes secret and public data either is redacted entirely or keeps its
secret in a separate redacted field type, as
Clientdoes above. - The name includes type arguments and module qualification as ordinary
printing would: a generic
Secret[T]printsSecret[i64](@redacted). - A generic wrapper over a redacted type is not itself redacted;
Holder[ApiKey]printsHolder[ApiKey](item=ApiKey(@redacted)). A@newtypeover a redacted type likewise prints its own name with the redacted field. - In an f-string,
!r,!s,!a,=, and every format spec give the sameApiKey(@redacted); there is no conversion that reveals the contents. Padding and alignment apply to the marker. - Explicit formatting is unaffected: a
displayordebugmethod the type defines is used byprint,str.reprand f-strings as for any other type, and astd.reportFormattableimplementation still writes what it chooses. - Field access, equality, hashing, ordering, matching, moves, and layout are
unchanged.
@redactedcombines with@copyand@newtypein any order.
Redaction is a guard against accidental logging, not a security boundary: code that can read a field can still print it.