Skip to content

Releases: pyozig/PyOZ

PyOZ v0.12.2

25 Mar 09:55
2236a2a

Choose a tag to compare

What's New in v0.12.2

Added

  • pyoz.MemoryView — New type for accepting Python memoryview objects. Provides read-only data: []const u8 access to the underlying buffer. Call .release() when done.
  • pyoz.BytesLike — New unified type that accepts Python bytes, bytearray, or memoryview. Provides read-only data: []const u8 regardless of source type. Call .release() when done (no-op for bytes/bytearray).
  • anytype and comptime limitations documented — The .from auto-scan guide now explains why functions with anytype or comptime parameters are skipped and shows the typed-wrapper workaround.
  • abi3 = true configuration documented — The [tool.pyoz] configuration reference now includes the abi3 option.
  • ByteArray, MemoryView, BytesLike in docs — Added to both the types guide and API reference.

Changed

  • Removed bridge module — user module is now the root modulepyoz init no longer generates a separate _pyoz_bridge.zig module. Instead, the comptime decl-analysis block is inlined directly in the user's lib.zig, making it the library's root module. This enables Zig root-module features like std_options (custom logging, panic handlers, etc.) that were previously inaccessible because the bridge was the root. The build.zig template is also simplified (no WriteFiles, no extra createModule).

Fixed

  • Private fields with non-zero-initializable types — Private fields (underscore prefix) whose types contain non-nullable pointers (e.g. std.heap.ArenaAllocator, std.mem.Allocator) no longer cause a compile error. Previously, all private fields were zero-initialized via std.mem.zeroes, which fails for types that cannot be set to zero. Now uses a smarter initDefault that: (1) uses the field's default value if one is defined in the struct, (2) zero-initializes if the type supports it, or (3) leaves the field as undefined for types that cannot be zeroed — the user's __new__ function must initialize these fields.
  • Integer overflow now raises OverflowError — Converting a Python int to a small Zig integer type (e.g. u8, i16, u32) now performs a range check and raises OverflowError if the value doesn't fit. Previously, values were silently truncated via @truncate (C-style wrapping), so u8 receiving 256 would silently become 0.
  • Class method errors now map to correct Python exceptions — Zig errors returned from class methods (__getitem__, __len__, __call__, __iter__, __next__, __repr__, __hash__, comparisons, number protocol, mapping protocol, etc.) are now mapped to their corresponding Python exception types via mapWellKnownError. Previously, all class method errors were hardcoded to RuntimeError. For example, error.IndexOutOfBounds now raises IndexError, error.ValueError raises ValueError, error.Overflow raises OverflowError, etc. Affects 13 error sites across 8 class protocol files.
  • ListView.get() sets IndexError/TypeErrorListView.get(index) now sets IndexError for out-of-bounds access and TypeError for element conversion failures, instead of returning silent null with no Python exception set.
  • Negative index on unsigned __getitem__ now raises IndexError — Classes with usize index in __getitem__ (mapping protocol) now raise IndexError instead of OverflowError when accessed with a negative index. Previously, Python's C API OverflowError from converting a negative int to unsigned was propagated directly.

Installation

Download the binary for your platform and add it to your PATH:

Platform Binary
Linux x86_64 pyoz-x86_64-linux
Linux ARM64 pyoz-aarch64-linux
macOS x86_64 pyoz-x86_64-macos
macOS ARM64 (Apple Silicon) pyoz-aarch64-macos
Windows x86_64 pyoz-x86_64-windows.exe
Windows ARM64 pyoz-aarch64-windows.exe

Source

Download PyOZ-0.12.2.tar.gz for the source code.

Quick Start

pyoz init mymodule
cd mymodule
pyoz build
pip install dist/*.whl

PyOZ v0.12.1

04 Mar 15:54
6973c4a

Choose a tag to compare

What's New in v0.12.1

Added

  • Auto-kwargs for ?T params in .from — Functions discovered via .from that have optional (?T) parameters now automatically support keyword arguments without requiring pyoz.Args(T). Required params stay positional-only, optional params become keyword-capable. For example, fn add(a: i64, b: i64, multiplier: ?i64) generates the Python signature add(a, b, /, multiplier=None) and can be called as add(1, 2, multiplier=5). Requires source text (via pyoz.withSource(), __source__(), or __params__) for parameter name extraction. A compile-time warning is emitted if ?T params are used without source text.
  • .funcs and .classes are now optional in pyoz.module() — When using .from for all declarations, you no longer need to specify empty .funcs = &.{} and .classes = &.{}.

Changed

  • Enum stubs show type annotations instead of values — Generated .pyi stubs for enums now use field: int / field: str instead of exposing the actual values (field = 0 / field = "name").

Fixed

  • Source parser performance: eagerly-parsed source cachingwithSource now eagerly parses the source file once and shares the pre-parsed data across all lookups, eliminating redundant comptime tokenization. Previously, Zig's comptime evaluator re-parsed the source for every doc/param lookup (92× for a 24-class file), causing 2m+ compile times. Now compiles in ~3s. Also eliminates source text leaking into debug symbols (the old SourceInfo("entire source...") generic type name is replaced with ParsedSource).
  • pub inline fn and pub noinline fn doc comments now extracted — The source parser now correctly skips inline, noinline, and export keywords between pub and fn, so doc comments on pub inline fn declarations are properly extracted for Python docstrings and stubs.
  • Package mode no longer requires underscore prefix in module-name — Package layout detection now uses py-packages containing the project name, instead of requiring the module name to start with _. Users can now use module-name = "liburing" with py-packages = ["liburing"] and from .liburing import * in __init__.py. The underscore convention still works but is no longer required. Affects pyoz develop, pyoz build, pyoz test, pyoz bench, and wheel building.
  • build.zig templates now include guidance for custom C include paths — The generated build.zig includes comments showing that addIncludePath and addObjectFile must be added to user_lib_mod, not lib.root_module (which is the bridge module in 0.12.0). This prevents @cImport failures when wrapping C libraries.
  • Better compile error for kwfunc without pyoz.Args(T) — When using explicit kwfunc with a parameter not wrapped in pyoz.Args(T), the compiler now shows a clear error message with the fix, instead of the cryptic type 'u32' has no members.

Installation

Download the binary for your platform and add it to your PATH:

Platform Binary
Linux x86_64 pyoz-x86_64-linux
Linux ARM64 pyoz-aarch64-linux
macOS x86_64 pyoz-x86_64-macos
macOS ARM64 (Apple Silicon) pyoz-aarch64-macos
Windows x86_64 pyoz-x86_64-windows.exe
Windows ARM64 pyoz-aarch64-windows.exe

Source

Download PyOZ-0.12.1.tar.gz for the source code.

Quick Start

pyoz init mymodule
cd mymodule
pyoz build
pip install dist/*.whl

PyOZ v0.12.0

03 Mar 23:54
3eecba8

Choose a tag to compare

What's New in v0.12.0

Added

  • Automatic PyInit_ export — zero boilerplate module initializationpyoz.module() now auto-exports the PyInit_<name> function via @export in a comptime block. The build system generates a bridge module that forces Zig's lazy analysis, so the user only needs pub const Module = pyoz.module(.{ .name = "mymod", ... }); — no manual pub export fn PyInit_mymod needed. Works with both standard and --package layouts. The module const must be pub. Existing projects with manual PyInit_ exports should remove them to avoid duplicate symbol errors.
  • .from auto-scan API — New module config field .from = &.{ @import("my_funcs.zig") } that auto-discovers and registers public declarations from Zig namespaces. Eliminates repetitive pyoz.func()/pyoz.class()/pyoz.constant() boilerplate when the Python name matches the Zig identifier. Supports functions, classes, enums, constants, and exceptions. Docstrings are provided via {name}__doc__ convention, or automatically extracted from /// doc comments when using pyoz.withSource(). Works with pyoz.source() for filtering (.only/.exclude) and pyoz.sub() for submodules.
  • pyoz.source(namespace, .{ .only = &.{"a", "b"} }) / .exclude — Filter which declarations from a .from namespace are exported. Use .only to whitelist or .exclude to blacklist specific names.
  • pyoz.sub("name", namespace) submodule support — Declare submodules from .from namespaces. Functions, constants, classes, and enums in the submodule namespace are registered under module.name.
  • pyoz.Exception(base, doc) and pyoz.ErrorMap() markers — Declare custom exceptions and error-to-exception mappings inside .from namespaces.
  • .from auto-detects pyoz.Args(T) for keyword arguments — Functions using pyoz.Args(T) in .from namespaces are automatically wrapped with named kwargs support, identical to explicit kwfunc registration.
  • .from deduplication — Explicit config entries (.funcs, .classes, etc.) always take priority over .from-scanned declarations with the same name. Duplicate names across multiple .from entries produce a compile error with guidance to use pyoz.source() filtering.
  • .from stub generation.pyi stubs are automatically generated for all .from-scanned declarations, including functions, classes, enums, and constants.
  • __text_signature__ support for help() and inspect.signature() — All functions (module-level and class methods) now embed a CPython Argument Clinic-style signature in ml_doc. help(func) shows proper parameter names instead of add(...). Keyword argument functions using pyoz.Args(T) show field names and defaults (e.g. safe_sqrt(value, default=None)). Class methods correctly use self/$type conventions. When withSource is used for a .from namespace, real Zig parameter names are used (e.g. count_words(s, /) instead of count_words(arg0, /)); without source, positional args fall back to arg0/arg1.
  • pyoz.withSource(@import("f.zig"), @embedFile("f.zig")) — comptime source introspection — New wrapper for .from entries that enables automatic extraction of /// doc comments as Python docstrings, //! module-level doc comments as module.__doc__, real function parameter names for __text_signature__ and .pyi stubs, and /// comments above structs as class __doc__. No boilerplate needed in the .from file — just wrap the @import at the module config site. Uses std.zig.Tokenizer at comptime — source text is NOT embedded in the final binary. Explicit {name}__doc__ and {name}__params__ constants still take priority for backward compatibility. A legacy per-file __source__ function/constant is also supported.

⚠️ Breaking Changes — Migration from 0.11.x

🔧 build.zig must be updated. The build system now uses a bridge module pattern to auto-export PyInit_. Projects created with pyoz init on 0.11.x need their build.zig replaced. The easiest way is to re-run pyoz init --path in your project directory (backs up existing files), or manually update build.zig to match the new template — see the generated build.zig for the current template.

🗑️ Remove manual PyInit_ exports. If your lib.zig contains pub export fn PyInit_mymod, delete it. The auto-export now handles this. Keeping it causes a exported symbol collision compile error.

🔓 Make the module const pub. Change const MyMod = pyoz.module(.{...}); to pub const MyMod = pyoz.module(.{...});. The bridge module needs to see it to trigger the auto-export.

🔄 kwfunc now requires pyoz.Args(T). The old kwfunc that accepted functions with ?T optional parameters is removed. Wrap your kwargs in a struct:

// Before (0.11.x)
fn greet(name: ?[]const u8, times: ?i32) []const u8 { ... }
pyoz.kwfunc("greet", greet, "Greet someone")

// After (0.12.0)
fn greet(args: pyoz.Args(struct { name: ?[]const u8 = null, times: ?i32 = null })) []const u8 {
    const name = args.value.name orelse "World";
    ...
}
pyoz.kwfunc("greet", greet, "Greet someone")

Changed

  • kwfunc renamed from kwfunc_named — The old kwfunc (which generated unusable arg0/arg1 kwarg names due to Zig's @typeInfo not exposing parameter names) is removed. kwfunc_named is renamed to kwfunc and is now the only way to register keyword argument functions. All kwargs functions must use pyoz.Args(T) for real parameter names.
  • Removed broken ?T kwargs auto-detection — Functions with optional ?T parameters are no longer auto-detected as keyword argument functions (in both .from and explicit registration). The ?T detection generated unusable arg0/arg1 names. Use pyoz.Args(T) instead for proper named kwargs support.

Fixed

  • .from enums with unsigned integer tags (enum(u8), enum(u16), etc.) registered as StrEnum instead of IntEnum — The isIntEnum detection in from.zig used a flawed heuristic (checking signedness + exhaustiveness) that only recognized signed tags like enum(i32). Unsigned explicit tags like enum(u8) fell through and were incorrectly registered as StrEnum, causing .value to return string names instead of integer values. Fixed by matching the proven logic from enums.zig — checking against standard integer types (u8, u16, u32, u64, i8, i16, i32, i64, isize, c_int, c_long), which correctly distinguishes user-specified tags from Zig's auto-generated non-standard bit-width tags (u1, u2, u3, ...).
  • Build-time PyInit_ symbol validationpyoz build/pyoz dev now validates that the compiled .so/.pyd exports the expected PyInit_<module_name> symbol after building. Catches mismatches between module-name in pyproject.toml and the Zig export function, printing a clear warning with the exact fix needed. Prevents the confusing ImportError: dynamic module does not define module export function at runtime.
  • Stub .pyi filename uses module-name instead of project name — When module-name differs from the project name (e.g., module-name = "_liburing" with name = "liburing"), the stub file was incorrectly named liburing.pyi instead of _liburing.pyi. Now uses config.getModuleName() so the stub filename matches the actual .so/.pyd.
  • py-packages now supports Python src-layoutpy-packages = ["mypkg"] now searches src/mypkg/ first (PEP 517 src-layout) before falling back to mypkg/ (flat layout). Previously only flat layout was supported, causing Warning: Python package directory not found for src-layout projects and missing __init__.py in wheels.
  • Stub generation crash on non-Args(T) kwfunc parameters@hasDecl in stubs.zig was called on non-struct types (e.g., ?[]const u8, ?bool) when generating stubs for functions with optional parameters, causing a comptime error. Now checks @typeInfo(...) == .@"struct" before calling @hasDecl.
  • PyPI wheel: _pyoz.so placed inside pyoz/ package — The native extension _pyoz.so was previously installed at the top level of site-packages/, polluting the namespace. Now placed inside pyoz/_pyoz.so with a relative import (from ._pyoz import ...), keeping the package self-contained.
  • PyPI pyoz CLI updated to pyoz.Args(T) for 0.12.0 compatibility — The pypi/src/lib.zig CLI wrapper functions used raw ?T optional parameters with kwfunc, which is no longer supported in 0.12.0. Updated to use pyoz.Args(T) structs. Also added bridge module to pypi/build.zig for auto-export.

Installation

Download the binary for your platform and add it to your PATH:

Platform Binary
Linux x86_64 pyoz-x86_64-linux
Linux ARM64 pyoz-aarch64-linux
macOS x86_64 pyoz-x86_64-macos
macOS ARM64 (Apple Silicon) pyoz-aarch64-macos
Windows x86_64 pyoz-x86_64-windows.exe
Windows ARM64 pyoz-aarch64-windows.exe

Source

Download PyOZ-0.12.0.tar.gz for the source code.

Quick Start

pyoz init mymodule
cd mymodule
pyoz build
pip install dist/*.whl

PyOZ v0.11.5

28 Feb 12:22
c51f3b0

Choose a tag to compare

What's New in v0.11.5

Fixed

  • Stub generation @setEvalBranchQuota exceeded with large modules - Modules with many functions and classes would fail to compile with evaluation exceeded 100000 backwards branches in stub, test, and benchmark generation. Fixed by raising @setEvalBranchQuota to std.math.maxInt(u32) in all comptime generation call sites: generateModuleStubs, generateImports in stubs.zig, and the stubs/tests/benchmarks section embedding plus qualified name generation in root.zig.
  • CLI linker error on systems with GCC 15+ - zig build cli failed with unhandled relocation type R_X86_64_PC64 on Linux systems with recent GCC/binutils (15+), which emit .sframe sections that Zig's linker cannot handle. Fixed by targeting musl for the CLI executable on Linux, using Zig's bundled musl instead of the system glibc toolchain. The CLI is now a fully static binary with zero external dependencies.
  • __del__ called on failed __new__ causing segfault - When a user-defined __new__ returned an error or null (e.g., via raiseValueError), PyOZ still called __del__ during deallocation of the failed object, leading to a segfault on uninitialized data. Now an _initialized flag is tracked on each object: it is set only when __init__/__new__ succeeds, and __del__ is skipped if the flag is unset. This matches Python semantics where __del__ is never called if __new__ raises. Works in both ABI3 and non-ABI3 modes.
  • __new__ error union errors always raised RuntimeError - When a class __new__ returning !T hit an error, it was always raised as RuntimeError regardless of the error name. Now uses the existing mapWellKnownError() function so error.OutOfMemory raises MemoryError, error.ValueError raises ValueError, error.IndexOutOfBounds raises IndexError, etc. — matching the behavior already in place for regular functions and methods since v0.11.0.

Installation

Download the binary for your platform and add it to your PATH:

Platform Binary
Linux x86_64 pyoz-x86_64-linux
Linux ARM64 pyoz-aarch64-linux
macOS x86_64 pyoz-x86_64-macos
macOS ARM64 (Apple Silicon) pyoz-aarch64-macos
Windows x86_64 pyoz-x86_64-windows.exe
Windows ARM64 pyoz-aarch64-windows.exe

Source

Download PyOZ-0.11.5.tar.gz for the source code.

Quick Start

pyoz init mymodule
cd mymodule
pyoz build
pip install dist/*.whl

PyOZ v0.11.4

19 Feb 10:14
2535170

Choose a tag to compare

What's New in v0.11.4

Added

  • pyoz.Signature(T, "python_type") -- stub return type override - New comptime wrapper type that overrides the Python type annotation in generated .pyi stubs without affecting runtime behavior. Use this when the Zig return type doesn't map cleanly to the desired Python type, most commonly when ?T is used for CPython exception signaling (returning null + PyErr_SetString) rather than representing Python None. For example, fn probe() pyoz.Signature(?Dict, "dict[str, bool]") generates def probe() -> dict[str, bool] instead of the incorrect def probe() -> dict[str, bool] | None. Also supports pyoz.Signature(?void, "Never") for functions that only raise. Works uniformly on module-level functions, class instance/static/class methods, __call__, __new__, and allowThreads/allowThreadsTry.
  • PyMemoryView_Check - Added type check function for memoryview objects, following the same isTypeOrSubtype pattern as other type checks. Uses PyMemoryView_Type which is part of the stable ABI since Python 3.2, so works across 3.8–3.13 in both normal and ABI3 modes.

Fixed

  • Comptime branch quota exceeded with large modules - Modules with many functions would fail to compile with evaluation exceeded 1000 backwards branches in anyFuncUsesDateTime/anyFuncUsesDecimal. Fixed by setting @setEvalBranchQuota(std.math.maxInt(u32)) in both functions.

Refactored

  • Type check functions - PySet_Check, PyFrozenSet_Check, PyBytes_Check, PyByteArray_Check, and PyObject_TypeCheck now use the shared isTypeOrSubtype helper for consistency.

Removed

  • method__returns__ class method stub override - The pub const method_name__returns__: []const u8 = "..." convention for overriding class method return type stubs has been removed in favor of the unified pyoz.Signature(T, "python_type") approach, which works identically for both module-level functions and class methods.

Installation

Download the binary for your platform and add it to your PATH:

Platform Binary
Linux x86_64 pyoz-x86_64-linux
Linux ARM64 pyoz-aarch64-linux
macOS x86_64 pyoz-x86_64-macos
macOS ARM64 (Apple Silicon) pyoz-aarch64-macos
Windows x86_64 pyoz-x86_64-windows.exe
Windows ARM64 pyoz-aarch64-windows.exe

Source

Download PyOZ-0.11.4.tar.gz for the source code.

Quick Start

pyoz init mymodule
cd mymodule
pyoz build
pip install dist/*.whl

PyOZ v0.11.3

10 Feb 22:22
33a6b37

Choose a tag to compare

What's New in v0.11.3

Fixed

  • pip-installed pyoz test/bench on Windows - Deduplicated test/bench runner logic in the pip package (pypi/src/lib.zig) by delegating to commands.runTests/commands.runBench instead of maintaining a separate copy. The previous duplicate code had hardcoded zig-out/lib/ paths and no package mode support, causing test failures on Windows.

Installation

Download the binary for your platform and add it to your PATH:

Platform Binary
Linux x86_64 pyoz-x86_64-linux
Linux ARM64 pyoz-aarch64-linux
macOS x86_64 pyoz-x86_64-macos
macOS ARM64 (Apple Silicon) pyoz-aarch64-macos
Windows x86_64 pyoz-x86_64-windows.exe
Windows ARM64 pyoz-aarch64-windows.exe

Source

Download PyOZ-0.11.3.tar.gz for the source code.

Quick Start

pyoz init mymodule
cd mymodule
pyoz build
pip install dist/*.whl

PyOZ v0.11.2

10 Feb 22:02
f6d6972

Choose a tag to compare

What's New in v0.11.2

Fixed

  • Windows build support - Fixed 77 lld-link: undefined symbol errors when building PyOZ projects on Windows. The generated build.zig template now accepts -Dpython-lib-dir and -Dpython-lib-name options, and pyoz build passes them automatically on Windows to link against python3.lib (stable ABI). Windows requires all symbols resolved at link time, unlike Linux/macOS which resolve Python symbols at runtime.
  • Windows output path - Fixed FileNotFound error during wheel creation on Windows. Zig places DLLs (.pyd) in zig-out/bin/ on Windows, not zig-out/lib/. The builder, test runner, and benchmark runner now use the correct output directory per platform.
  • Package mode test/bench imports - In package layout (module name starts with _), the generated test and benchmark scripts now also import ravn (the package name) in addition to import _ravn, so users can write assert ravn.add(2, 3) == 5 in their tests. The test/bench runners also detect package mode, copy the .pyd/.so into the package directory, and add the project root to PYTHONPATH.
  • ASCII tree output for pyoz init - Replaced UTF-8 box-drawing characters with ASCII in the project structure output, fixing garbled display on Windows PowerShell.

Installation

Download the binary for your platform and add it to your PATH:

Platform Binary
Linux x86_64 pyoz-x86_64-linux
Linux ARM64 pyoz-aarch64-linux
macOS x86_64 pyoz-x86_64-macos
macOS ARM64 (Apple Silicon) pyoz-aarch64-macos
Windows x86_64 pyoz-x86_64-windows.exe
Windows ARM64 pyoz-aarch64-windows.exe

Source

Download PyOZ-0.11.2.tar.gz for the source code.

Quick Start

pyoz init mymodule
cd mymodule
pyoz build
pip install dist/*.whl

PyOZ v0.11.1

10 Feb 13:56
8939791

Choose a tag to compare

What's New in v0.11.1

Added

  • Multi-phase module initialization (PEP 489) - PyOZ now uses PyModuleDef_Init + Py_mod_exec slot instead of the legacy PyModule_Create single-phase init. This is required for sub-interpreter support (PEP 554) and is the modern standard for Python extension modules. Simple modules (return Module.init()) work unchanged. Modules that need post-init work (e.g., adding submodules) should use the new .module_init callback in the module config instead of doing work after init() in PyInit_*.

Fixed

  • get_X/set_X computed properties no longer exposed as methods - When a class defines get_user_data() and set_user_data(), PyOZ correctly creates a user_data property but previously also exposed get_user_data() and set_user_data() as callable methods, cluttering the API. Now computed property accessors are filtered from the method table (methods.zig) and stub generation (stubs.zig), so only the X property appears in Python. The filter correctly handles: get_X as computed property getter, set_X with matching get_X as computed property setter, and set_X as field setter override. Standalone set_X without a matching getter or field is still exposed as a method.

Installation

Download the binary for your platform and add it to your PATH:

Platform Binary
Linux x86_64 pyoz-x86_64-linux
Linux ARM64 pyoz-aarch64-linux
macOS x86_64 pyoz-x86_64-macos
macOS ARM64 (Apple Silicon) pyoz-aarch64-macos
Windows x86_64 pyoz-x86_64-windows.exe
Windows ARM64 pyoz-aarch64-windows.exe

Source

Download PyOZ-0.11.1.tar.gz for the source code.

Quick Start

pyoz init mymodule
cd mymodule
pyoz build
pip install dist/*.whl

PyOZ v0.11.0

10 Feb 00:14
a2ffa34

Choose a tag to compare

What's New in v0.11.0

Added

  • pyoz init --package -- Python package directory layout - New --package flag for pyoz init that scaffolds a project with a proper Python package directory. Instead of installing a flat .so directly into site-packages, the extension is placed inside a package directory with an __init__.py that re-exports all native symbols. The native module is automatically prefixed with an underscore (e.g., _myproject.so) to avoid name collisions with the package directory. pyoz build and pyoz develop automatically detect package mode when module-name starts with _ and a py-packages entry matches the project name, placing the .so and .pyi inside the package directory in wheels and development installs. This enables combining native extensions with pure Python code in the same importable package.
  • pyoz.Owned(T) -- allocator-backed return types - New generic wrapper for returning heap-allocated data from Zig functions and methods. Owned(T) pairs a value with its allocator; PyOZ converts the inner value to a Python object then automatically frees the backing memory. This eliminates the need for fixed-size stack buffers when building dynamic strings or data. The pyoz.owned(allocator, value) constructor auto-coerces mutable slices ([]u8) to const ([]const u8), so std.fmt.allocPrint results can be returned directly without @as casts. Supports all return type wrappers: !Owned(T) (error union), ?Owned(T) (optional). Works with any slice type that toPy handles.
  • pyoz.fmt() -- inline string formatter - New utility function for formatting strings using Zig's std.fmt syntax. Returns a [*:0]const u8 suitable for passing to PyErr_SetString, raise functions, or any API that copies the string immediately. The 4096-byte buffer lives in the caller's stack frame (the function is inline), so it is safe to use in one-liners like return pyoz.raiseValueError(pyoz.fmt("value {d} exceeds limit {d}", .{ val, limit })). Eliminates the need for manual bufPrintZ boilerplate when building dynamic error messages.
  • pyoz.base(Parent) -- single inheritance between PyOZ classes - New function for declaring that one PyOZ-defined Zig struct inherits from another. The child struct declares pub const __base__ = pyoz.base(Animal); and embeds the parent as _parent: Animal (must be the first field). PyOZ sets tp_base to the parent's type object so isinstance(), Python's MRO, and method/property inheritance all work automatically. The child's __init__ accepts a flattened argument list (parent fields first, then child fields). Parent methods and properties are inherited via MRO — no duplication needed. Works in both non-ABI3 (static type object) and ABI3 (PyType_FromSpecWithBases) modes. Comptime validation ensures correct struct layout and parent registration order. Stub generation emits class Dog(Animal): with the correct flattened __init__ signature.
  • pyoz test -- inline embedded tests - New CLI command that builds the module, extracts embedded Python test code from the compiled .so, and runs it with unittest (stdlib, zero dependencies). Tests are defined inline in the Zig module definition using pyoz.@"test"("name", \\body) for assertion tests and pyoz.testRaises("name", "ExceptionType", \\body) for exception tests. The generated Python file uses unittest.TestCase with proper assertRaises context managers. Supports --verbose/-v for detailed output and --release/-r to build in release mode before testing.
  • pyoz bench -- inline embedded benchmarks - New CLI command that builds the module in release mode, extracts embedded Python benchmark code, and runs it with timeit (stdlib). Benchmarks are defined inline using pyoz.bench("name", \\body). The generated script times each benchmark over 100,000 iterations and prints a formatted results table with ops/s. Both commands are available in the Zig CLI (src/cli) and Python wrapper (pyoz test / pyoz bench).
  • pyoz.TestDef and pyoz.BenchDef types - New struct types for defining inline tests and benchmarks. pyoz.@"test"() creates assertion-based tests, pyoz.testRaises() creates exception-checking tests, and pyoz.bench() creates benchmarks. These are passed to pyoz.module() via the new .tests and .benchmarks optional config fields.
  • Binary section embedding for tests and benchmarks - Test and benchmark Python code is generated at comptime and embedded into the compiled .so as named sections (.pyoztest / .pyozbenc on ELF/PE, __DATA,__pyoztest / __DATA,__pyozbenc on Mach-O), using the same magic-header pattern as stubs (PYOZTEST / PYOZBENC + 8-byte LE length + content).
  • Generic section extraction in symreader.zig - New extractNamedSection() infrastructure that parameterizes section name and magic string across ELF/PE/Mach-O formats. extractTests() and extractBenchmarks() are thin wrappers. Existing extractStubs() is unchanged.
  • Syntax checking before test/bench execution - pyoz test and pyoz bench now run python3 -m py_compile on the generated Python file before executing it. If the user's inline test/benchmark code has syntax errors, a clear error message with line numbers is shown instead of a confusing runtime traceback.

Fixed

  • __hash__ correctness for classes defining __eq__ - When a class defines __eq__ (or any comparison dunder) without explicitly defining __hash__, PyOZ now sets tp_hash = PyObject_HashNotImplemented, making instances correctly unhashable (raises TypeError on hash(), cannot be added to sets or used as dict keys). Previously, these classes silently retained the default id-based hash, violating Python semantics. This fix works for both ABI3 and non-ABI3 modes. Classes that define both __eq__ and __hash__ continue to work as before.
  • Computed property setters returning ?void or !void caused compile error - When a set_X computed property setter returned an optional (?void) or error union (!void) instead of plain void, the generated wrapper in properties.zig discarded the return value, which Zig rejects for non-void types. This prevented using the return pyoz.raiseValueError("msg") one-liner pattern in property setters. All three setter code paths (generateSetter for field-based custom setters, generateComputedSetter for computed properties, and generatePyozPropertySetter for pyoz.property() API setters) now handle ?void, !void, and plain void return types using the same three-branch dispatch pattern used throughout the rest of the codebase (attributes.zig, descriptor.zig, sequence.zig, etc.). Also fixed generateSetter's existing error union branch to preserve already-set Python exceptions instead of overwriting them.
  • Zig errors now map to correct Python exception types - Previously, all Zig errors (including error.TypeError, error.IndexOutOfBounds, error.DivisionByZero, etc.) were incorrectly raised as RuntimeError in Python. Now setError() in wrappers.zig and setErrorFromMapping() in errors.zig use a new mapWellKnownError() function that first tries an exact match against all ExcBase enum variants (covering all 50+ standard Python exceptions like TypeError, ValueError, IndexError, KeyError, ZeroDivisionError, AttributeError, FileNotFoundError, PermissionError, MemoryError, NotImplementedError, StopIteration, etc.), then checks common Zig-idiomatic aliases (DivisionByZero -> ZeroDivisionError, OutOfMemory -> MemoryError, IndexOutOfBounds -> IndexError, KeyNotFound -> KeyError, FileNotFound -> FileNotFoundError, PermissionDenied -> PermissionError, etc.), and falls back to RuntimeError only for truly unrecognized errors.

Installation

Download the binary for your platform and add it to your PATH:

Platform Binary
Linux x86_64 pyoz-x86_64-linux
Linux ARM64 pyoz-aarch64-linux
macOS x86_64 pyoz-x86_64-macos
macOS ARM64 (Apple Silicon) pyoz-aarch64-macos
Windows x86_64 pyoz-x86_64-windows.exe
Windows ARM64 pyoz-aarch64-windows.exe

Source

Download PyOZ-0.11.0.tar.gz for the source code.

Quick Start

pyoz init mymodule
cd mymodule
pyoz build
pip install dist/*.whl

PyOZ v0.10.5

09 Feb 01:13
b25599c

Choose a tag to compare

What's New in v0.10.5

Added

  • pyoz.Ref(T) -- strong Python object references - New generic type that allows one PyOZ-managed Zig struct to hold a strong reference to another Python object, preventing use-after-free when the referenced object is garbage collected. Ref(T) wraps a ?*PyObject with automatic Py_IncRef on set() and Py_DecRef on clear() and object deallocation. Ref fields are automatically excluded from Python properties, __init__ parameters, stub generation, and auto-doc signatures. Freelist-safe: references are released in tp_dealloc before freelist push, and std.mem.zeroes on pop ensures no double-free.
  • Module.selfObject(T, ptr) helper - Recovers the wrapping *PyObject from a *const T data pointer using compile-time offset math. Used to obtain the PyObject needed for Ref(T).set() from within methods that receive self: *const T.

Installation

Download the binary for your platform and add it to your PATH:

Platform Binary
Linux x86_64 pyoz-x86_64-linux
Linux ARM64 pyoz-aarch64-linux
macOS x86_64 pyoz-x86_64-macos
macOS ARM64 (Apple Silicon) pyoz-aarch64-macos
Windows x86_64 pyoz-x86_64-windows.exe
Windows ARM64 pyoz-aarch64-windows.exe

Source

Download PyOZ-0.10.5.tar.gz for the source code.

Quick Start

pyoz init mymodule
cd mymodule
pyoz build
pip install dist/*.whl