Skip to content

Python Classes

This page describes how to define, instantiate and customize Python classes with Pydust.

Classes are defined by wrapping structs with the py.class function.

1
2
3
4
5
pub const SomeClass = py.class(struct {
    pub const __doc__ = "Some class defined in Zig accessible from Python";

    count: u32 = 0,
});

Struct fields are used to store per-instance state, and public struct functions are exported as Python functions. See the section on functions for more details.

Instantiation

By default, Pydust classes can only be instantiated from Zig. While it is possible to create Pydust structs with Zig syntax, this will only create a Zig struct and not the corresponding Python object around it.

For example, the class above can be correctly constructed using the py.init function:

const some_class = try py.init(SomeClass, .{ .count = 1 });

From Python

To enable instantiation from Python, you must define a __new__ function that takes a CallArgs struct and returns a new instance of Self.

1
2
3
4
5
6
7
pub const ConstructableClass = py.class(struct {
    count: u32 = 0,

    pub fn __new__(args: struct { count: u32 }) !@This() {
        return .{ .count = args.count };
    }
});
1
2
3
4
def test_constructor():
    from example import classes

    assert isinstance(classes.ConstructableClass(20), classes.ConstructableClass)

Inheritance

Inheritance allows you to define a subclass of another Zig Pydust class.

Note

It is currently not possible to create a subclass of a Python class.

Subclasses are defined by including the parent class struct as a field of the subclass struct. They can then be instantiated from Zig using py.init, or from Python if a __new__ function is defined.

pub const Animal = py.class(struct {
    const Self = @This();

    species: py.PyString,

    pub fn species(self: *Self) py.PyString {
        return self.species;
    }
});

pub const Dog = py.class(struct {
    const Self = @This();

    animal: Animal,
    breed: py.PyString,

    pub fn __new__(args: struct { breed: py.PyString }) !Self {
        args.breed.incref();
        return .{
            .animal = .{ .species = try py.PyString.create("dog") },
            .breed = args.breed,
        };
    }

    pub fn breed(self: *Self) py.PyString {
        return self.breed;
    }
});
1
2
3
4
5
def test_subclasses():
    d = classes.Dog("labrador")
    assert d.breed() == "labrador"
    assert d.species() == "dog"
    assert isinstance(d, classes.Animal)

Super

The py.super(Type, self) function returns a proxy py.PyObject that can be used to invoke methods on the super class. This behaves the same as the Python builtin super.

Properties

Properties behave the same as the Python @property decorator. They allow you to define getter and setter functions for an attribute of the class.

Pydust properties are again defined as structs with optional get and set methods. If you do not define a set method for example, then the property is read-only. And vice versa.

In this example we define an email property that performs a naive validity check. It makes use of Zig's @fieldParentPointer builtin to get a handle on the class instance struct.

pub const User = py.class(struct {
    const Self = @This();

    pub fn __new__(args: struct { name: py.PyString }) !Self {
        args.name.incref();
        return .{ .name = args.name, .email = .{} };
    }

    name: py.PyString,
    email: py.property(struct {
        const Prop = @This();

        e: ?py.PyString = null,

        pub fn get(prop: *const Prop) !?py.PyString {
            return prop.e;
        }

        pub fn set(prop: *Prop, value: py.PyString) !void {
            const self: *Self = @fieldParentPtr(Self, "email", prop);
            if (std.mem.indexOfScalar(u8, try value.asSlice(), '@') == null) {
                return py.ValueError.raiseFmt("Invalid email address for {s}", .{try self.name.asSlice()});
            }
            prop.e = value;
        }
    }),
});
def test_properties():
    u = classes.User("Dave")
    assert u.email is None

    u.email = "dave@dave.com"
    assert u.email == "dave@dave.com"

    with pytest.raises(ValueError) as exc_info:
        u.email = "dave"
    assert str(exc_info.value) == "Invalid email address for Dave"

Instance Attributes

Attributes are similar to properties, except they do not allow for custom getters and setters. Due to how they are implemented, attributes wrap the type in a struct definition:

struct { value: T }

This means you must access the attribute in Zig using .value.

pub const Counter = py.class(struct {
    const Self = @This();

    count: py.attribute(usize) = .{ .value = 0 },

    pub fn __new__(args: struct {}) !Self {
        _ = args;
        return .{};
    }

    pub fn increment(self: *Self) void {
        self.count.value += 1;
    }
});
1
2
3
4
5
6
def test_attributes():
    c = classes.Counter()
    assert c.count == 0
    c.increment()
    c.increment()
    assert c.count == 2

Note

Attributes are currently read-only. Please file an issue if you have a use-case for writable attributes.

Class Attributes

Class attributes are not currently supported by Pydust.

Static Methods

Static methods are similar to class methods but do not have access to the class itself. You can define static methods by simply not taking a self argument.

1
2
3
4
5
pub const Math = py.class(struct {
    pub fn add(args: struct { x: i32, y: i32 }) i32 {
        return args.x + args.y;
    }
});

Dunder Methods

Dunder methods, or "double underscore" methods, provide a mechanism for overriding builtin Python operators.

  • object refers to either a pointer to a Pydust type, a py.PyObject, or any other Pydust Python type, e.g. py.PyString.
  • CallArgs refers to a Zig struct that is interpreted as args and kwargs where fields are marked as keyword arguments if they have a default value.

Also note the shorthand signatures:

const binaryfunc = fn(*Self, other: object) !object;

Type Methods

Method Signature
__new__ fn(CallArgs) !Self
__init__ fn(*Self, CallArgs) !void
__del__ fn(*Self) void
__repr__ fn(*Self) !py.PyString
__str__ fn(*Self) !py.PyString
__iter__ fn(*Self) !object
__next__ fn(*Self) !?object

Sequence Methods

Method Signature
__len__ fn(*Self) !usize

The remaining sequence methods are yet to be implemented.

Mapping Methods

Method Signature
__getitem__ binaryfunc

The remaining mapping methods are yet to be implemented.

Number Methods

Method Signature
__add__ binaryfunc
__sub__ binaryfunc
__mul__ binaryfunc
__mod__ binaryfunc
__divmod__ binaryfunc
__pow__ binaryfunc
__lshift__ binaryfunc
__rshift__ binaryfunc
__and__ binaryfunc
__or__ binaryfunc
__xor__ binaryfunc
__truediv__ binaryfunc
__floordiv__ binaryfunc
__matmul__ binaryfunc

Buffer Methods

Method Signature
__buffer__ fn (*Self, *py.PyBuffer, flags: c_int)
__release_buffer__ fn (*Self, *py.PyBuffer)