github pyozig/PyOZ v0.13.0
PyOZ v0.13.0

latest release: v0.13.1
2 hours ago

What's New in v0.13.0

Breaking: requires Zig 0.16.0 and Python 3.10+; existing projects need a few build.zig edits (including one line for macOS); pyoz.fmt returns pyoz.Formatted (see Changed); wheels are now portable and tagged manylinux_2_17_* / macosx_13_0_*. Step-by-step instructions: Upgrading to 0.13.

Added

  • Upgrading to 0.13 guide, verified by migrating a project generated by the 0.12 CLI.
  • Editable installs (PEP 660). pip install -e . works through pyoz.backend (new build_editable hook), and pyoz develop now builds and pip-installs a standard editable wheel (__editable__.*.pth + dist-info) instead of symlinking into site-packages. Rebuilds are picked up without reinstalling; pip uninstall removes it.
  • Portable wheels, tagged from the binary. pyoz build builds wheels for a baseline CPU, glibc 2.17 on Linux (manylinux_2_17_*, accepted by PyPI) and macOS 13.0, and reads the platform tag from the built module (highest glibc symbol version, allowed shared libraries, minimum macOS version, architecture), like auditwheel. --native builds for the exact machine. linux-platform-tag selects the glibc floor (manylinux_2_28_x86_64 builds against glibc 2.28). (#58)
  • Cross-building wheels: pyoz build --target <targets>|all --python <3.X[t]>. One machine builds wheels for Linux, macOS and Windows on x86_64 and aarch64, for any CPython 3.10–3.14 including free-threaded 3.13t/3.14t. Headers and Windows import libraries for other platforms or versions come from python-build-standalone, verified against its SHA256SUMS and cached.
  • Complete wheel metadata from [project] (PEP 621/639): readme (file or text, any content type), SPDX license expressions and legacy license tables, license-files shipped in .dist-info/licenses/ (by default LICEN[CS]E*, COPYING*, NOTICE*, AUTHORS*), classifiers, keywords, authors, maintainers, URLs, dependencies, optional-dependencies, and entry_points.txt from [project.scripts]/gui-scripts/entry-points. pyoz publish sends the same metadata, so PyPI shows the classifiers and license. Parsing uses a real TOML reader (multi-line arrays, inline tables). (#58)
  • Source installs without Zig: when no Zig 0.16 is on PATH, pyoz.backend requests the ziglang package in pip's isolated build environment and builds with its compiler, so pip install git+... and sdists work anywhere. pyoz build warns when zig is a different minor release. (#60)
  • pyoz.Args(...) keyword arguments on class methods (instance, static and class methods), with defaults shown in help() and fields expanded in stubs. Contributed by @marselester. (#59)
  • .withParams("a, b") on pyoz.func entries names Python-visible parameters in stubs and help() (previously always arg0, arg1). (#61)
  • Async protocols on classes: __aiter__, __anext__, __await__, __aenter__, __aexit__. Instances work with async for, await obj and async with, and anext(it, default) works as well. Plain Zig return values become awaitables that complete immediately and need no running event loop, so they work under asyncio, trio or a bare coro.send(None). Returning a pyoz.asyncFn result runs the work on a std.Io task, and in __anext__ a null result from the task ends the iteration. The new pyoz.Future(f) names that result type. The slots take the per-object lock on free-threaded builds, work in ABI3 mode, and generate async def / Generator[...] stubs.
  • pyoz.asyncMethod: async instance methods. The self parameter type is the safety contract, enforced at compile time: self: T runs on a copy taken at call time (pointer-free fields); self: *const T borrows the object, which is kept alive until the Zig task is joined, and is only allowed on __frozen__ classes with no *T methods; self: *T is rejected.
  • Async functions on std.Io: pyoz.asyncFn(f). Calling the function from Python returns an asyncio.Future; f runs on its own std.Io task without the GIL. Optional leading std.Io and std.mem.Allocator (per-call arena) parameters. Cancelling the Python task cancels the Zig task (error.Canceled at its next Io call). Bounded thread use (pyoz.setAsyncConcurrency, default 256) with unbounded queued concurrency. About 3x the throughput of run_in_executor for bulk gather. Works on regular, free-threaded and ABI3 builds.
  • Free-threading stress tests in the suite: on free-threaded interpreters, many threads hammer one object (ArrayList appends that race on reallocation, plus read-modify-write), share one iterator through its __next__ slot and one async iterator through __anext__, and six event loops run async jobs in parallel; they run on multi-core CI runners in Debug and ReleaseSafe and skip on GIL builds.
  • Free-threaded CPython (PEP 703) support. .gil_used = false module option declares GIL-free operation (previously every PyOZ import re-enabled the GIL). Every class method, property and protocol slot runs in a per-object critical section on free-threaded builds (__lock__ = false opts out); zero cost on regular builds. pyoz build produces cpXY-cpXYt wheels. CI tests 3.14t.
  • pyoz.io(), pyoz.asyncLiveJobs(), pyoz.Formatted.
  • Async functions can return PyOZ class instances and take pointer-free structs (including PyOZ classes) by value; up to 8 parameters; module .error_mappings apply to async errors; stubs emit Awaitable[T]. Tested on asyncio and uvloop, and cross-linked for Windows (x86_64/ARM64), macOS (x86_64/arm64) and Linux (x86_64/aarch64).

Changed

  • Python 3.10 is now the minimum (3.8 and 3.9 are end-of-life). ABI3 builds use Py_LIMITED_API = 0x030A0000 and produce cp310-abi3 wheels; generated projects declare requires-python = ">=3.10"; CI tests 3.10–3.14 and 3.14t. abi.zig's version checks now derive from the same constant (they previously read a build option no build script defined).
  • Zig 0.16.0 is now required. The library, build scripts, CLI, pypi/ backend and the project templates generated by pyoz init all target Zig 0.16 (std.Io, std.process.Init, module-level link_libc/linkSystemLibrary). Projects created with an older pyoz init need the same build.zig changes: .link_libc = true on the module instead of lib.linkLibC(), and addLibraryPath/linkSystemLibrary(name, .{}) called on the module.
  • Pure-Zig wheel compression. The vendored miniz C library is removed; DEFLATE now uses std.compress.flate. The CLI is a single static binary with no libc dependency.
  • pyoz init no longer runs zig build twice. The package fingerprint is generated directly and the dependency hash is pinned with zig fetch --save. Generated build.zig.zon files declare minimum_zig_version = "0.16.0".
  • pyoz test and pyoz bench share one implementation.
  • PyPI upload errors show the server's message instead of only the HTTP status, with a hint for common causes; they no longer claim "the version already exists" for every HTTP 400. The Python pyoz command exits with the error instead of a traceback. (#58)
  • Keyword-argument errors follow CPython: f() got an unexpected keyword argument 'x' (previously unknown keywords were silently ignored), got multiple values for argument 'x', missing required argument 'x', takes at most N positional arguments. A specific conversion error (e.g. OverflowError) is no longer replaced by a generic TypeError, help() shows the real defaults (exponent=2.0, not exponent=None), and Path/buffer arguments passed through pyoz.Args are released after the call. (#59)
  • Fixed-size array parameters accept tuples as well as lists, and stubs say list[T] | tuple[T, ...]. A wrong length raises ValueError: expected N items, got M (previously a TypeError whose message was the Zig error name). (#61)
  • The Zig version is defined once (version.zig); a CLI test checks every other copy (build.zig.zon files, the pip backend, CI workflows, installation docs).
  • Python 3.14 supported and tested.
  • pyoz.fmt is now lazy and lifetime-safe (breaking). It returns a pyoz.Formatted value that is rendered by its consumer (every raise* function, and any return-value conversion to str). Code that declared a [*:0]const u8 return type for pyoz.fmt results must use pyoz.Formatted("fmt", struct { ... }) (and return .{ .args = .{ ... } }) instead; this is a compile error, not a silent change.

Fixed

  • Examples returned pointers into dead stack frames (join_strings, decimal_double in both examples), and one returned a shared static buffer that races under free-threading (iter_join). They passed in Debug but failed in release builds with Zig 0.16's optimizer. They now return heap memory via pyoz.Owned; Owned(pyoz.Decimal) is supported. CI now also runs every test suite in ReleaseSafe.
  • pip install . failed for every project using pyoz.backend (ModuleNotFoundError: No module named '_pyoz': the backend imported the native module by the wrong name). CI now builds the pyoz wheel and runs pip install . and pip install -e . on a generated project.
  • pyoz publish only uploads wheels for the current name and version; leftovers from earlier builds in dist/ are skipped instead of failing with HTTP 400.
  • Windows ReleaseSafe builds failed inside Zig 0.16's translation of MinGW's _FORTIFY_SOURCE wrappers; _FORTIFY_SOURCE is now undefined for the Python header import on Windows.
  • pyoz.fmt returned a pointer into a dead stack frame when used as a __repr__/__str__ return value. Messages longer than 4 KB were replaced by "fmt: message too long"; they are now rendered in full. repr via pyoz.fmt is ~12% faster.
  • Free-threaded builds: static object headers wrote 1 into ob_tid; the class freelist raced (now disabled on free-threaded builds); lazy datetime/decimal/pathlib caches raced (now lock-free), including an ABI3 datetime ordering bug where a class could be used before it was published; Python library name missed the t ABI flag.
  • Wheel RECORD is now spec-compliant. Every file is listed with its sha256= digest and size; previously entries had no hashes.
  • Wheel and .dist-info names are normalized (My-Pkg → my_pkg) as the binary distribution format requires.
  • Reproducible wheels: ZIP timestamps honor SOURCE_DATE_EPOCH.
  • Cross-compiling generated projects to Windows now installs name.pyd (the template chose the extension from the host OS instead of the target).
  • Windows wheels with nested Python packages use / in ZIP entry names instead of \.
  • ABI3 builds under Zig 0.16: built-in type objects (PyLong_Type, ...) are referenced via @extern, since 0.16's C translator rejects extern variables of opaque type under Py_LIMITED_API.
  • ABI3 classes defining __eq__ without __hash__ failed to compile (PyObject_HashNotImplemented was cast as a function body rather than a pointer).
  • Double free in Python detection when sysconfig returned an empty include path.
  • Child processes killed by a signal no longer trip a union-field safety check in pyoz build/test/bench/develop.
  • TestPyPI uploads prefer TEST_PYPI_TOKEN over PYPI_TOKEN when both are set.
  • pyoz init with an invalid name no longer leaves an empty directory behind.
  • Generated README referenced a nonexistent pyoz build-wheel command.
  • macOS builds of generated projects failed to link (undefined symbol: _PyErr_Occurred, ...): the template never allowed the interpreter-provided C API symbols to be undefined. New projects set linker_allow_shlib_undefined on macOS; existing ones need the one-line change in the upgrading guide.
  • macOS wheels were uninstallable: the tag used the build machine's full macOS version (e.g. macosx_14_5_arm64), but pip only generates macosx_N_0 tags for macOS 11 and later.
  • Linux wheels were not portable: built for the build machine's CPU features and glibc, tagged linux_* (rejected by PyPI), and the documented manylinux override only relabelled them. The pyoz package's own wheels were tagged manylinux2014/macosx_11_0 while built against glibc 2.28/2.34 and macOS 13.
  • Windows non-ABI3 modules linked python3.lib, which only exports the Stable ABI; they now link python3XY[t].lib.
  • Free-threaded builds: protocol slots were not locked. __repr__, __iter__/__next__, operators, __getitem__/__setitem__, __call__, rich comparison and the buffer protocol ran without the object's critical section on free-threaded CPython (only methods and properties took it). A new stress test drives __next__ from many threads.
  • Explicit-target builds on Debian/Ubuntu could not find the multiarch pyconfig.h; PyOZ's build.zig now provides it.
  • sdists include py-packages, README and license files, skip build output, and use the normalized file name (PEP 625).

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.13.0.tar.gz for the source code.

Quick Start

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

Don't miss a new PyOZ release

NewReleases is sending notifications on new releases.