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_instance = try py.init(SomeClass, .{ .count = 1 });

Alternatively, a class can be allocated without instantiation. This can be useful when some fields of the class refer by pointer to other fields.

1
2
3
4
5
var some_instance = try py.alloc(SomeClass);
some_instance.* = .{
    .foo = 124,
    .bar = &some_instance.foo,
};

From Python

Declaring a __init__ function signifies to Pydust to make your class instantiable from Python. This function may take zero arguments as a pure marker to allow instantiation.

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

    pub fn __init__(self: *@This(), args: struct { count: u32 }) void {
        self.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 __init__ 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 __init__(self: *Self, args: struct { breed: py.PyString }) !void {
        args.breed.incref();
        self.* = .{
            .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 __init__(self: *Self, args: struct { name: py.PyString }) void {
        args.name.incref();
        self.* = .{ .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 {
            if (prop.e) |e| e.incref();
            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()});
            }
            value.incref();
            prop.e = value;
        }
    }),

    greeting: py.property(struct {
        pub fn get(self: *const Self) !py.PyString {
            return py.PyString.createFmt("Hello, {s}!", .{try self.name.asSlice()});
        }
    }) = .{},

    pub fn __del__(self: *Self) void {
        self.name.decref();
        if (self.email.e) |e| e.decref();
    }
});
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"

    assert u.greeting == "Hello, Dave!"

In the second example, the greeting property takes *const Self as a first parameter providing it direct access to the outer struct. This is a convenience when implementing typically getter-only properties.

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 __init__(self: *Self) void {
        _ = self;
    }

    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;
    }
});

Zig Only Methods

Classes can define methods that are not exposed to python via py.zig wrapper

pub const ZigOnlyMethod = py.class(struct {
    const Self = @This();
    number: i32,

    pub fn __init__(self: *Self, args: struct { x: i32 }) void {
        self.number = args.x;
    }

    pub usingnamespace py.zig(struct {
        pub fn get_number(self: *const Self) i32 {
            return self.number;
        }
    });

    pub fn reexposed(self: *const Self) i32 {
        return self.get_number();
    }
});

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:

1
2
3
const binaryfunc = fn(*Self, object) !object;
const unaryfunc = fn(*Self) !object;
const inquiry = fn(*Self) !bool;

Type Methods

Method Signature
__init__ fn() void
__init__ fn(*Self) !void
__init__ fn(*Self, CallArgs) !void
__del__ fn(*Self) void
__repr__ fn(*Self) !py.PyString
__str__ fn(*Self) !py.PyString
__call__ fn(*Self, CallArgs) !py.PyObject
__iter__ fn(*Self) !object
__next__ fn(*Self) !?object
__getattr__ fn(*Self, object) !?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.

Rich Compare

Method Signature
__lt__ fn(*Self, object) !bool
__le__ fn(*Self, object) !bool
__eq__ fn(*Self, object) !bool
__ne__ fn(*Self, object) !bool
__gt__ fn(*Self, object) !bool
__ge__ fn(*Self, object) !bool

Note

By default, __ne__ will delegate to the negation of __eq__ if it is defined.

Pydust also defines py.CompareOp representing the CPython comparison operators allowing you to implement the full comparison logic in a single __richcompare__ function.

Method Signature
__hash__ fn(*Self) !usize
__richcompare__ fn(*Self, other: object, CompareOp) !usize

Tip

Whenever __eq__ is implemented, it is advisable to also implement __hash__.

Number Methods

Method Signature
__add__ binaryfunc
__iadd__ binaryfunc
__sub__ binaryfunc
__isub__ binaryfunc
__mul__ binaryfunc
__imul__ binaryfunc
__mod__ binaryfunc
__imod__ binaryfunc
__divmod__ binaryfunc
__pow__ binaryfunc
__ipow__ binaryfunc
__lshift__ binaryfunc
__ilshift__ binaryfunc
__rshift__ binaryfunc
__irshift__ binaryfunc
__and__ binaryfunc
__iand__ binaryfunc
__or__ binaryfunc
__ior__ binaryfunc
__xor__ binaryfunc
__ixor__ binaryfunc
__truediv__ binaryfunc
__itruediv__ binaryfunc
__floordiv__ binaryfunc
__ifloordiv__ binaryfunc
__matmul__ binaryfunc
__imatmul__ binaryfunc
__neg__ unaryfunc
__pos__ unaryfunc
__abs__ unaryfunc
__invert__ unaryfunc
__int__ unaryfunc
__float__ unaryfunc
__index__ unaryfunc
__bool__ inquiry

Note

When implementing in place variants of the functions make sure to incref reference to self as your function is supposed to return a new reference, per CPython documentation

Dynamic dispatch example
pub const Operator = py.class(struct {
    const Self = @This();

    num: u64,

    pub fn __init__(self: *Self, args: struct { num: u64 }) void {
        self.num = args.num;
    }

    pub fn num(self: *const Self) u64 {
        return self.num;
    }

    pub fn __truediv__(self: *const Self, other: py.PyObject) !py.PyObject {
        const selfCls = try py.self(Self);
        defer selfCls.decref();

        if (try py.PyFloat.check(other)) {
            const numF: f64 = @floatFromInt(self.num);
            return py.create(numF / try py.as(f64, other));
        } else if (try py.PyLong.check(other)) {
            return py.create(self.num / try py.as(u64, other));
        } else if (try py.isinstance(other, selfCls)) { // TODO(ngates): #193
            const otherO: *Self = try py.as(*Self, other);
            return py.object(try py.init(Self, .{ .num = self.num / otherO.num }));
        } else {
            return py.TypeError.raise("Unsupported number type for Operator division");
        }
    }
});
All numerical methods example
pub const Ops = py.class(struct {
    const Self = @This();

    num: u64,

    pub fn __init__(self: *Self, args: struct { num: u64 }) !void {
        self.num = args.num;
    }

    pub fn num(self: *const Self) u64 {
        return self.num;
    }

    pub fn __add__(self: *const Self, other: *const Self) !*Self {
        return py.init(Self, .{ .num = self.num + other.num });
    }

    pub fn __iadd__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num += other.num;
        return self;
    }

    pub fn __sub__(self: *const Self, other: *const Self) !*Self {
        return py.init(Self, .{ .num = self.num - other.num });
    }

    pub fn __isub__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num -= other.num;
        return self;
    }

    pub fn __mul__(self: *const Self, other: *const Self) !*Self {
        return py.init(Self, .{ .num = self.num * other.num });
    }

    pub fn __imul__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num *= other.num;
        return self;
    }

    pub fn __mod__(self: *const Self, other: *const Self) !*Self {
        return py.init(Self, .{ .num = try std.math.mod(u64, self.num, other.num) });
    }

    pub fn __imod__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num = try std.math.mod(u64, self.num, other.num);
        return self;
    }

    pub fn __divmod__(self: *const Self, other: *const Self) !py.PyTuple {
        return py.PyTuple.create(.{ self.num / other.num, std.math.mod(u64, self.num, other.num) });
    }

    pub fn __pow__(self: *const Self, other: *const Self) !*Self {
        return py.init(Self, .{ .num = std.math.pow(u64, self.num, other.num) });
    }

    pub fn __ipow__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num = std.math.pow(u64, self.num, other.num);
        return self;
    }

    pub fn __lshift__(self: *const Self, other: *const Self) !*Self {
        py.incref(self);
        return py.init(Self, .{ .num = self.num << @as(u6, @intCast(other.num)) });
    }

    pub fn __ilshift__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num = self.num << @as(u6, @intCast(other.num));
        return self;
    }

    pub fn __rshift__(self: *const Self, other: *const Self) !*Self {
        py.incref(self);
        return py.init(Self, .{ .num = self.num >> @as(u6, @intCast(other.num)) });
    }

    pub fn __irshift__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num = self.num >> @as(u6, @intCast(other.num));
        return self;
    }

    pub fn __and__(self: *const Self, other: *const Self) !*Self {
        return py.init(Self, .{ .num = self.num & other.num });
    }

    pub fn __iand__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num = self.num & other.num;
        return self;
    }

    pub fn __xor__(self: *const Self, other: *const Self) !*Self {
        return py.init(Self, .{ .num = self.num ^ other.num });
    }

    pub fn __ixor__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num = self.num ^ other.num;
        return self;
    }

    pub fn __or__(self: *const Self, other: *const Self) !*Self {
        return py.init(Self, .{ .num = self.num | other.num });
    }

    pub fn __ior__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num = self.num | other.num;
        return self;
    }

    pub fn __truediv__(self: *const Self, other: *const Self) !*Self {
        return py.init(Self, .{ .num = self.num / other.num });
    }

    pub fn __itruediv__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num = self.num / other.num;
        return self;
    }

    pub fn __floordiv__(self: *const Self, other: *const Self) !*Self {
        return py.init(Self, .{ .num = self.num / other.num });
    }

    pub fn __ifloordiv__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num = self.num / other.num;
        return self;
    }

    pub fn __matmul__(self: *const Self, other: *const Self) !*Self {
        return py.init(Self, .{ .num = self.num * other.num });
    }

    pub fn __imatmul__(self: *Self, other: *const Self) !*Self {
        py.incref(self);
        self.num *= other.num;
        return self;
    }
});

Buffer Methods

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