Skip to content

Quick Start

This guide walks you through creating your first PyOZ module using the pyoz CLI.

Prerequisites

pip install pyoz

This installs the pyoz CLI with prebuilt binaries for all major platforms.

Option 2: Download Prebuilt Binaries

Download the latest pyoz binary for your platform from the GitHub Releases page and add it to your PATH.

Option 3: Build from Source

git clone https://github.com/dzonerzy/PyOZ.git
cd PyOZ
zig build cli

The pyoz binary will be in zig-out/bin/. Add it to your PATH or use the full path.

Your First Module

Step 1: Initialize a Project

pyoz init mymodule
cd mymodule

This creates a project with:

mymodule/
├── build.zig
├── build.zig.zon
├── pyproject.toml
├── README.md
├── .gitignore
└── src/
    └── lib.zig       # Your module code

Step 2: Edit Your Module

Open src/lib.zig - it already contains a starter template:

const pyoz = @import("PyOZ");

// ============================================================================
// Define your functions here
// ============================================================================

/// Add two integers
fn add(a: i64, b: i64) i64 {
    return a + b;
}

/// Multiply two floats
fn multiply(a: f64, b: f64) f64 {
    return a * b;
}

/// Greet someone by name
fn greet(name: []const u8) ![]const u8 {
    _ = name;
    return "Hello from mymodule!";
}

// ============================================================================
// Module definition
// ============================================================================

pub const Module = pyoz.module(.{
    .name = "mymodule",
    .doc = "mymodule - A Python extension module built with PyOZ",
    .funcs = &.{
        pyoz.func("add", add, "Add two integers"),
        pyoz.func("multiply", multiply, "Multiply two floats"),
        pyoz.func("greet", greet, "Return a greeting"),
    },
    .classes = &.{},
});

Step 3: Build the Wheel

pyoz build

This compiles your module and creates a wheel in dist/:

dist/
└── mymodule-0.1.0-cp310-cp310-linux_x86_64.whl

Step 4: Install and Test

pip install dist/mymodule-*.whl
python -c "import mymodule; print(mymodule.add(2, 3))"  # 5

Development Workflow

PyOZ provides several commands to streamline your workflow:

Command Description
pyoz build Build a debug wheel
pyoz build --release Build an optimized release wheel
pyoz develop Build and install in development mode (symlinked)
pyoz test Run embedded inline tests
pyoz test -v Run tests with verbose output
pyoz bench Run embedded benchmarks (release mode)
pyoz publish Publish wheel(s) to PyPI

For detailed CLI options, see the CLI Reference.

Adding a Class

Edit src/lib.zig to add a class:

const pyoz = @import("PyOZ");
const std = @import("std");

// Define a struct - it becomes a Python class
const Point = struct {
    x: f64,
    y: f64,

    // Methods take *const Self or *Self as first parameter
    pub fn magnitude(self: *const Point) f64 {
        return @sqrt(self.x * self.x + self.y * self.y);
    }

    pub fn scale(self: *Point, factor: f64) void {
        self.x *= factor;
        self.y *= factor;
    }

    // Static methods don't take self
    pub fn origin() Point {
        return .{ .x = 0.0, .y = 0.0 };
    }
};

pub const Module = pyoz.module(.{
    .name = "mymodule",
    .doc = "Module with a Point class",
    .funcs = &.{},
    .classes = &.{
        pyoz.class("Point", Point),
    },
});

Use it from Python:

import mymodule

# Create instances (fields become __init__ parameters)
p = mymodule.Point(3.0, 4.0)
print(p.x, p.y)           # 3.0 4.0
print(p.magnitude())      # 5.0

# Mutating methods work
p.scale(2.0)
print(p.x, p.y)           # 6.0 8.0

# Static methods
origin = mymodule.Point.origin()
print(origin.x, origin.y) # 0.0 0.0

Error Handling

Zig errors automatically become Python exceptions:

fn divide(a: f64, b: f64) !f64 {
    if (b == 0.0) {
        return error.DivisionByZero;
    }
    return a / b;
}

pub const MyModule = pyoz.module(.{
    .name = "mymodule",
    .funcs = &.{
        pyoz.func("divide", divide, "Divide two numbers"),
    },
});

PyOZ automatically maps well-known Zig error names to the correct Python exception. error.DivisionByZero becomes ZeroDivisionError:

import mymodule

try:
    mymodule.divide(10, 0)
except ZeroDivisionError as e:
    print(f"Error: {e}")  # Error: DivisionByZero

For custom error names, use explicit error mappings.

Module Constants

Add compile-time constants to your module:

pub const MyModule = pyoz.module(.{
    .name = "mymodule",
    .funcs = &.{},
    .consts = &.{
        pyoz.constant("VERSION", "1.0.0"),
        pyoz.constant("PI", 3.14159265358979),
        pyoz.constant("MAX_SIZE", @as(i64, 1000)),
        pyoz.constant("DEBUG", false),
    },
});
import mymodule

print(mymodule.VERSION)   # "1.0.0"
print(mymodule.PI)        # 3.14159265358979
print(mymodule.MAX_SIZE)  # 1000
print(mymodule.DEBUG)     # False

Enums

Define Python enums from Zig enums:

// Integer enum (becomes IntEnum)
const Color = enum(i32) {
    Red = 1,
    Green = 2,
    Blue = 3,
};

// String enum (becomes StrEnum) - no explicit integer type
const Status = enum {
    pending,
    active,
    completed,
};

pub const MyModule = pyoz.module(.{
    .name = "mymodule",
    .funcs = &.{},
    // Auto-detects IntEnum vs StrEnum based on tag type
    .enums = &.{
        pyoz.enumDef("Color", Color),
        pyoz.enumDef("Status", Status),
    },
});
import mymodule

# Color is an IntEnum
print(mymodule.Color.Red)        # Color.Red
print(mymodule.Color.Red.value)  # 1

# Status is a StrEnum
print(mymodule.Status.pending)        # Status.pending
print(mymodule.Status.pending.value)  # "pending"

Next Steps