Releases: pyozig/PyOZ
PyOZ v0.12.2
What's New in v0.12.2
Added
pyoz.MemoryView— New type for accepting Pythonmemoryviewobjects. Provides read-onlydata: []const u8access to the underlying buffer. Call.release()when done.pyoz.BytesLike— New unified type that accepts Pythonbytes,bytearray, ormemoryview. Provides read-onlydata: []const u8regardless of source type. Call.release()when done (no-op for bytes/bytearray).anytypeandcomptimelimitations documented — The.fromauto-scan guide now explains why functions withanytypeorcomptimeparameters are skipped and shows the typed-wrapper workaround.abi3 = trueconfiguration documented — The[tool.pyoz]configuration reference now includes theabi3option.- ByteArray, MemoryView, BytesLike in docs — Added to both the types guide and API reference.
Changed
- Removed bridge module — user module is now the root module —
pyoz initno longer generates a separate_pyoz_bridge.zigmodule. Instead, the comptime decl-analysis block is inlined directly in the user'slib.zig, making it the library's root module. This enables Zig root-module features likestd_options(custom logging, panic handlers, etc.) that were previously inaccessible because the bridge was the root. Thebuild.zigtemplate is also simplified (noWriteFiles, no extracreateModule).
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 viastd.mem.zeroes, which fails for types that cannot be set to zero. Now uses a smarterinitDefaultthat: (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 asundefinedfor types that cannot be zeroed — the user's__new__function must initialize these fields. - Integer overflow now raises
OverflowError— Converting a Pythonintto a small Zig integer type (e.g.u8,i16,u32) now performs a range check and raisesOverflowErrorif the value doesn't fit. Previously, values were silently truncated via@truncate(C-style wrapping), sou8receiving 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 viamapWellKnownError. Previously, all class method errors were hardcoded toRuntimeError. For example,error.IndexOutOfBoundsnow raisesIndexError,error.ValueErrorraisesValueError,error.OverflowraisesOverflowError, etc. Affects 13 error sites across 8 class protocol files. ListView.get()sets IndexError/TypeError —ListView.get(index)now setsIndexErrorfor out-of-bounds access andTypeErrorfor element conversion failures, instead of returning silentnullwith no Python exception set.- Negative index on unsigned
__getitem__now raisesIndexError— Classes withusizeindex in__getitem__(mapping protocol) now raiseIndexErrorinstead ofOverflowErrorwhen accessed with a negative index. Previously, Python's C APIOverflowErrorfrom 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/*.whlPyOZ v0.12.1
What's New in v0.12.1
Added
- Auto-kwargs for
?Tparams in.from— Functions discovered via.fromthat have optional (?T) parameters now automatically support keyword arguments without requiringpyoz.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 signatureadd(a, b, /, multiplier=None)and can be called asadd(1, 2, multiplier=5). Requires source text (viapyoz.withSource(),__source__(), or__params__) for parameter name extraction. A compile-time warning is emitted if?Tparams are used without source text. .funcsand.classesare now optional inpyoz.module()— When using.fromfor all declarations, you no longer need to specify empty.funcs = &.{}and.classes = &.{}.
Changed
- Enum stubs show type annotations instead of values — Generated
.pyistubs for enums now usefield: int/field: strinstead of exposing the actual values (field = 0/field = "name").
Fixed
- Source parser performance: eagerly-parsed source caching —
withSourcenow 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 oldSourceInfo("entire source...")generic type name is replaced withParsedSource). pub inline fnandpub noinline fndoc comments now extracted — The source parser now correctly skipsinline,noinline, andexportkeywords betweenpubandfn, so doc comments onpub inline fndeclarations are properly extracted for Python docstrings and stubs.- Package mode no longer requires underscore prefix in
module-name— Package layout detection now usespy-packagescontaining the project name, instead of requiring the module name to start with_. Users can now usemodule-name = "liburing"withpy-packages = ["liburing"]andfrom .liburing import *in__init__.py. The underscore convention still works but is no longer required. Affectspyoz develop,pyoz build,pyoz test,pyoz bench, and wheel building. build.zigtemplates now include guidance for custom C include paths — The generatedbuild.zigincludes comments showing thataddIncludePathandaddObjectFilemust be added touser_lib_mod, notlib.root_module(which is the bridge module in 0.12.0). This prevents@cImportfailures when wrapping C libraries.- Better compile error for
kwfuncwithoutpyoz.Args(T)— When using explicitkwfuncwith a parameter not wrapped inpyoz.Args(T), the compiler now shows a clear error message with the fix, instead of the cryptictype '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/*.whlPyOZ v0.12.0
What's New in v0.12.0
Added
- Automatic
PyInit_export — zero boilerplate module initialization —pyoz.module()now auto-exports thePyInit_<name>function via@exportin a comptime block. The build system generates a bridge module that forces Zig's lazy analysis, so the user only needspub const Module = pyoz.module(.{ .name = "mymod", ... });— no manualpub export fn PyInit_mymodneeded. Works with both standard and--packagelayouts. The module const must bepub. Existing projects with manualPyInit_exports should remove them to avoid duplicate symbol errors. .fromauto-scan API — New module config field.from = &.{ @import("my_funcs.zig") }that auto-discovers and registers public declarations from Zig namespaces. Eliminates repetitivepyoz.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 usingpyoz.withSource(). Works withpyoz.source()for filtering (.only/.exclude) andpyoz.sub()for submodules.pyoz.source(namespace, .{ .only = &.{"a", "b"} })/.exclude— Filter which declarations from a.fromnamespace are exported. Use.onlyto whitelist or.excludeto blacklist specific names.pyoz.sub("name", namespace)submodule support — Declare submodules from.fromnamespaces. Functions, constants, classes, and enums in the submodule namespace are registered undermodule.name.pyoz.Exception(base, doc)andpyoz.ErrorMap()markers — Declare custom exceptions and error-to-exception mappings inside.fromnamespaces..fromauto-detectspyoz.Args(T)for keyword arguments — Functions usingpyoz.Args(T)in.fromnamespaces are automatically wrapped with named kwargs support, identical to explicitkwfuncregistration..fromdeduplication — Explicit config entries (.funcs,.classes, etc.) always take priority over.from-scanned declarations with the same name. Duplicate names across multiple.fromentries produce a compile error with guidance to usepyoz.source()filtering..fromstub generation —.pyistubs are automatically generated for all.from-scanned declarations, including functions, classes, enums, and constants.__text_signature__support forhelp()andinspect.signature()— All functions (module-level and class methods) now embed a CPython Argument Clinic-style signature inml_doc.help(func)shows proper parameter names instead ofadd(...). Keyword argument functions usingpyoz.Args(T)show field names and defaults (e.g.safe_sqrt(value, default=None)). Class methods correctly useself/$typeconventions. WhenwithSourceis used for a.fromnamespace, real Zig parameter names are used (e.g.count_words(s, /)instead ofcount_words(arg0, /)); without source, positional args fall back toarg0/arg1.pyoz.withSource(@import("f.zig"), @embedFile("f.zig"))— comptime source introspection — New wrapper for.fromentries that enables automatic extraction of///doc comments as Python docstrings,//!module-level doc comments asmodule.__doc__, real function parameter names for__text_signature__and.pyistubs, and///comments above structs as class__doc__. No boilerplate needed in the.fromfile — just wrap the@importat the module config site. Usesstd.zig.Tokenizerat 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
kwfuncrenamed fromkwfunc_named— The oldkwfunc(which generated unusablearg0/arg1kwarg names due to Zig's@typeInfonot exposing parameter names) is removed.kwfunc_namedis renamed tokwfuncand is now the only way to register keyword argument functions. All kwargs functions must usepyoz.Args(T)for real parameter names.- Removed broken
?Tkwargs auto-detection — Functions with optional?Tparameters are no longer auto-detected as keyword argument functions (in both.fromand explicit registration). The?Tdetection generated unusablearg0/arg1names. Usepyoz.Args(T)instead for proper named kwargs support.
Fixed
.fromenums with unsigned integer tags (enum(u8),enum(u16), etc.) registered as StrEnum instead of IntEnum — TheisIntEnumdetection infrom.zigused a flawed heuristic (checking signedness + exhaustiveness) that only recognized signed tags likeenum(i32). Unsigned explicit tags likeenum(u8)fell through and were incorrectly registered as StrEnum, causing.valueto return string names instead of integer values. Fixed by matching the proven logic fromenums.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 validation —pyoz build/pyoz devnow validates that the compiled.so/.pydexports the expectedPyInit_<module_name>symbol after building. Catches mismatches betweenmodule-nameinpyproject.tomland the Zig export function, printing a clear warning with the exact fix needed. Prevents the confusingImportError: dynamic module does not define module export functionat runtime. - Stub
.pyifilename usesmodule-nameinstead of projectname— Whenmodule-namediffers from the project name (e.g.,module-name = "_liburing"withname = "liburing"), the stub file was incorrectly namedliburing.pyiinstead of_liburing.pyi. Now usesconfig.getModuleName()so the stub filename matches the actual.so/.pyd. py-packagesnow supports Python src-layout —py-packages = ["mypkg"]now searchessrc/mypkg/first (PEP 517 src-layout) before falling back tomypkg/(flat layout). Previously only flat layout was supported, causingWarning: Python package directory not foundfor src-layout projects and missing__init__.pyin wheels.- Stub generation crash on non-
Args(T)kwfunc parameters —@hasDeclinstubs.zigwas 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.soplaced insidepyoz/package — The native extension_pyoz.sowas previously installed at the top level ofsite-packages/, polluting the namespace. Now placed insidepyoz/_pyoz.sowith a relative import (from ._pyoz import ...), keeping the package self-contained. - PyPI
pyozCLI updated topyoz.Args(T)for 0.12.0 compatibility — Thepypi/src/lib.zigCLI wrapper functions used raw?Toptional parameters withkwfunc, which is no longer supported in 0.12.0. Updated to usepyoz.Args(T)structs. Also added bridge module topypi/build.zigfor 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/*.whlPyOZ v0.11.5
What's New in v0.11.5
Fixed
- Stub generation
@setEvalBranchQuotaexceeded with large modules - Modules with many functions and classes would fail to compile withevaluation exceeded 100000 backwards branchesin stub, test, and benchmark generation. Fixed by raising@setEvalBranchQuotatostd.math.maxInt(u32)in all comptime generation call sites:generateModuleStubs,generateImportsinstubs.zig, and the stubs/tests/benchmarks section embedding plus qualified name generation inroot.zig. - CLI linker error on systems with GCC 15+ -
zig build clifailed withunhandled relocation type R_X86_64_PC64on Linux systems with recent GCC/binutils (15+), which emit.sframesections that Zig's linker cannot handle. Fixed by targetingmuslfor 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 ornull(e.g., viaraiseValueError), PyOZ still called__del__during deallocation of the failed object, leading to a segfault on uninitialized data. Now an_initializedflag 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 raisedRuntimeError- When a class__new__returning!Thit an error, it was always raised asRuntimeErrorregardless of the error name. Now uses the existingmapWellKnownError()function soerror.OutOfMemoryraisesMemoryError,error.ValueErrorraisesValueError,error.IndexOutOfBoundsraisesIndexError, 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/*.whlPyOZ v0.11.4
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.pyistubs without affecting runtime behavior. Use this when the Zig return type doesn't map cleanly to the desired Python type, most commonly when?Tis used for CPython exception signaling (returningnull+PyErr_SetString) rather than representing PythonNone. For example,fn probe() pyoz.Signature(?Dict, "dict[str, bool]")generatesdef probe() -> dict[str, bool]instead of the incorrectdef probe() -> dict[str, bool] | None. Also supportspyoz.Signature(?void, "Never")for functions that only raise. Works uniformly on module-level functions, class instance/static/class methods,__call__,__new__, andallowThreads/allowThreadsTry.PyMemoryView_Check- Added type check function formemoryviewobjects, following the sameisTypeOrSubtypepattern as other type checks. UsesPyMemoryView_Typewhich 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 branchesinanyFuncUsesDateTime/anyFuncUsesDecimal. Fixed by setting@setEvalBranchQuota(std.math.maxInt(u32))in both functions.
Refactored
- Type check functions -
PySet_Check,PyFrozenSet_Check,PyBytes_Check,PyByteArray_Check, andPyObject_TypeChecknow use the sharedisTypeOrSubtypehelper for consistency.
Removed
method__returns__class method stub override - Thepub const method_name__returns__: []const u8 = "..."convention for overriding class method return type stubs has been removed in favor of the unifiedpyoz.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/*.whlPyOZ v0.11.3
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 tocommands.runTests/commands.runBenchinstead of maintaining a separate copy. The previous duplicate code had hardcodedzig-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/*.whlPyOZ v0.11.2
What's New in v0.11.2
Fixed
- Windows build support - Fixed 77
lld-link: undefined symbolerrors when building PyOZ projects on Windows. The generatedbuild.zigtemplate now accepts-Dpython-lib-dirand-Dpython-lib-nameoptions, andpyoz buildpasses them automatically on Windows to link againstpython3.lib(stable ABI). Windows requires all symbols resolved at link time, unlike Linux/macOS which resolve Python symbols at runtime. - Windows output path - Fixed
FileNotFounderror during wheel creation on Windows. Zig places DLLs (.pyd) inzig-out/bin/on Windows, notzig-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 alsoimport ravn(the package name) in addition toimport _ravn, so users can writeassert ravn.add(2, 3) == 5in their tests. The test/bench runners also detect package mode, copy the.pyd/.sointo the package directory, and add the project root toPYTHONPATH. - 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/*.whlPyOZ v0.11.1
What's New in v0.11.1
Added
- Multi-phase module initialization (PEP 489) - PyOZ now uses
PyModuleDef_Init+Py_mod_execslot instead of the legacyPyModule_Createsingle-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_initcallback in the module config instead of doing work afterinit()inPyInit_*.
Fixed
get_X/set_Xcomputed properties no longer exposed as methods - When a class definesget_user_data()andset_user_data(), PyOZ correctly creates auser_dataproperty but previously also exposedget_user_data()andset_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 theXproperty appears in Python. The filter correctly handles:get_Xas computed property getter,set_Xwith matchingget_Xas computed property setter, andset_Xas field setter override. Standaloneset_Xwithout 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/*.whlPyOZ v0.11.0
What's New in v0.11.0
Added
pyoz init --package-- Python package directory layout - New--packageflag forpyoz initthat scaffolds a project with a proper Python package directory. Instead of installing a flat.sodirectly into site-packages, the extension is placed inside a package directory with an__init__.pythat 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 buildandpyoz developautomatically detect package mode whenmodule-namestarts with_and apy-packagesentry matches the project name, placing the.soand.pyiinside 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. Thepyoz.owned(allocator, value)constructor auto-coerces mutable slices ([]u8) to const ([]const u8), sostd.fmt.allocPrintresults can be returned directly without@ascasts. Supports all return type wrappers:!Owned(T)(error union),?Owned(T)(optional). Works with any slice type thattoPyhandles.pyoz.fmt()-- inline string formatter - New utility function for formatting strings using Zig'sstd.fmtsyntax. Returns a[*:0]const u8suitable for passing toPyErr_SetString, raise functions, or any API that copies the string immediately. The 4096-byte buffer lives in the caller's stack frame (the function isinline), so it is safe to use in one-liners likereturn pyoz.raiseValueError(pyoz.fmt("value {d} exceeds limit {d}", .{ val, limit })). Eliminates the need for manualbufPrintZboilerplate 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 declarespub const __base__ = pyoz.base(Animal);and embeds the parent as_parent: Animal(must be the first field). PyOZ setstp_baseto the parent's type object soisinstance(), 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 emitsclass 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 withunittest(stdlib, zero dependencies). Tests are defined inline in the Zig module definition usingpyoz.@"test"("name", \\body)for assertion tests andpyoz.testRaises("name", "ExceptionType", \\body)for exception tests. The generated Python file usesunittest.TestCasewith properassertRaisescontext managers. Supports--verbose/-vfor detailed output and--release/-rto 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 withtimeit(stdlib). Benchmarks are defined inline usingpyoz.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.TestDefandpyoz.BenchDeftypes - New struct types for defining inline tests and benchmarks.pyoz.@"test"()creates assertion-based tests,pyoz.testRaises()creates exception-checking tests, andpyoz.bench()creates benchmarks. These are passed topyoz.module()via the new.testsand.benchmarksoptional config fields.- Binary section embedding for tests and benchmarks - Test and benchmark Python code is generated at comptime and embedded into the compiled
.soas named sections (.pyoztest/.pyozbencon ELF/PE,__DATA,__pyoztest/__DATA,__pyozbencon Mach-O), using the same magic-header pattern as stubs (PYOZTEST/PYOZBENC+ 8-byte LE length + content). - Generic section extraction in
symreader.zig- NewextractNamedSection()infrastructure that parameterizes section name and magic string across ELF/PE/Mach-O formats.extractTests()andextractBenchmarks()are thin wrappers. ExistingextractStubs()is unchanged. - Syntax checking before test/bench execution -
pyoz testandpyoz benchnow runpython3 -m py_compileon 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 setstp_hash = PyObject_HashNotImplemented, making instances correctly unhashable (raisesTypeErroronhash(), 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
?voidor!voidcaused compile error - When aset_Xcomputed property setter returned an optional (?void) or error union (!void) instead of plainvoid, the generated wrapper inproperties.zigdiscarded the return value, which Zig rejects for non-void types. This prevented using thereturn pyoz.raiseValueError("msg")one-liner pattern in property setters. All three setter code paths (generateSetterfor field-based custom setters,generateComputedSetterfor computed properties, andgeneratePyozPropertySetterforpyoz.property()API setters) now handle?void,!void, and plainvoidreturn types using the same three-branch dispatch pattern used throughout the rest of the codebase (attributes.zig,descriptor.zig,sequence.zig, etc.). Also fixedgenerateSetter'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 asRuntimeErrorin Python. NowsetError()inwrappers.zigandsetErrorFromMapping()inerrors.ziguse a newmapWellKnownError()function that first tries an exact match against allExcBaseenum variants (covering all 50+ standard Python exceptions likeTypeError,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 toRuntimeErroronly 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/*.whlPyOZ v0.10.5
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?*PyObjectwith automaticPy_IncRefonset()andPy_DecRefonclear()and object deallocation. Ref fields are automatically excluded from Python properties,__init__parameters, stub generation, and auto-doc signatures. Freelist-safe: references are released intp_deallocbefore freelist push, andstd.mem.zeroeson pop ensures no double-free.Module.selfObject(T, ptr)helper - Recovers the wrapping*PyObjectfrom a*const Tdata pointer using compile-time offset math. Used to obtain the PyObject needed forRef(T).set()from within methods that receiveself: *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