Upgrading to 0.13¶
PyOZ 0.13 moves to Zig 0.16 and Python 3.10+, and changes
pyoz.fmt. Most projects need three small edits: the version pins, a few lines
in build.zig, and any pyoz.fmt return types. Everything else is additive.
Checklist
- Install Zig 0.16.0 and use Python 3.10 or newer.
- Update the PyOZ dependency in
build.zig.zonand addminimum_zig_version. - Apply the
build.zigchanges:linkLibC,addLibraryPath,linkSystemLibrary, the.pyd/.soextension check, and one line for macOS. - Set
requires-python = ">=3.10"inpyproject.toml. - If a function returns
pyoz.fmt(...), change its return type topyoz.Formatted. - Rebuild:
pyoz build, thenpyoz test.
Requirements¶
| 0.12 | 0.13 | |
|---|---|---|
| Zig | 0.15.x | 0.16.0 |
| Python | 3.8 – 3.13 | 3.10 – 3.14, including free-threaded 3.14t |
| ABI3 wheels | cp38-abi3 |
cp310-abi3 |
build.zig.zon¶
Point the dependency at the latest 0.13 release and refresh its hash with zig fetch,
which rewrites build.zig.zon for you:
Then declare the minimum Zig version, so older compilers fail with a clear message instead of confusing errors:
.{
.name = .myproject,
.version = "0.1.0",
.fingerprint = 0x...,
.minimum_zig_version = "0.16.0", // add this
.dependencies = .{ .PyOZ = .{ ... } },
...
}
If you use a local checkout (.path = "..."), just update that checkout.
build.zig¶
Zig 0.16 moved libc and library linking from the compile step to the module.
These are the lines pyoz init generated in 0.12 that need to change.
Link libc on the module, not the library:
// Before (0.12)
const user_lib_mod = b.createModule(.{
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
.strip = strip,
.imports = &.{ .{ .name = "PyOZ", .module = pyoz_dep.module("PyOZ") } },
});
// ...
lib.linkLibC();
// After (0.13)
const user_lib_mod = b.createModule(.{
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
.strip = strip,
.link_libc = true, // replaces lib.linkLibC()
.imports = &.{ .{ .name = "PyOZ", .module = pyoz_dep.module("PyOZ") } },
});
Library path and system library also go on the module; linkSystemLibrary
takes an options argument:
// After (0.13)
user_lib_mod.addLibraryPath(.{ .cwd_relative = lib_dir });
user_lib_mod.linkSystemLibrary(lib_name, .{});
The same applies to anything else you added on lib: addIncludePath,
addObjectFile, addCSourceFile and friends are now called on the module
(user_lib_mod) instead.
Pick the extension from the target, not the host. The 0.12 template used
builtin.os.tag, which is the OS running the build, so cross-compiling to
Windows produced name.so. With the target's OS, the result is runtime-known,
so ++ becomes b.fmt:
// Before (0.12)
const builtin = @import("builtin");
// ...
const ext = if (builtin.os.tag == .windows) ".pyd" else ".so";
const install = b.addInstallArtifact(lib, .{
.dest_sub_path = "myproject" ++ ext,
});
// After (0.13)
const ext = if (target.result.os.tag == .windows) ".pyd" else ".so";
const install = b.addInstallArtifact(lib, .{
.dest_sub_path = b.fmt("myproject{s}", .{ext}),
});
(const builtin = @import("builtin"); can be removed if nothing else uses it.)
Allow undefined symbols on macOS. An extension gets the Python C API from
the interpreter that loads it, so the macOS linker must accept those symbols as
undefined (-undefined dynamic_lookup). The 0.12 template lacked this line;
without it macOS builds, native or cross-compiled, fail with
undefined symbol: _PyErr_Occurred and similar. Add it after b.addLibrary:
Starting fresh
For heavily customized projects it can be quicker to run
pyoz init --path in a scratch directory and copy your additions into the
newly generated build.zig.
pyproject.toml¶
If you set abi3 = true, wheels are now tagged cp310-abi3 and work on
Python 3.10 and every later version. ABI3 cannot target free-threaded Python;
pyoz build reports this if you try.
pyoz.fmt returns a lazy value¶
pyoz.fmt no longer returns a [*:0]const u8. It returns a
pyoz.Formatted value that is rendered by whoever consumes it. The old version
returned a pointer into a stack buffer, which was invalid once the function
that called pyoz.fmt returned (for example from __repr__), and it truncated
messages over 4 KB.
Passing it to a raise function needs no change:
Returning it needs a new return type. The compiler will point at each place:
// Before (0.12)
pub fn __repr__(self: *const Vec2) [*:0]const u8 {
return pyoz.fmt("Vec2({d:.2}, {d:.2})", .{ self.x, self.y });
}
// After (0.13): the format string lives in the return type
pub fn __repr__(self: *const Vec2) pyoz.Formatted("Vec2({d:.2}, {d:.2})", struct { f64, f64 }) {
return .{ .args = .{ self.x, self.y } };
}
The tuple type lists the argument types in order. If you need a
[*:0]const u8 for your own C calls, format into your own buffer with
std.fmt.bufPrintZ.
Behavior changes (no code changes needed)¶
pyoz developnow installs a standard PEP 660 editable wheel with pip instead of symlinking into site-packages. Runpip uninstall <name>once to remove an old symlink-based install ifpip listdoesn't show it, then delete any leftover<module>.sosymlink in site-packages.pip install -e .now works too.pip install .works withbuild-backend = "pyoz.backend"(it failed withModuleNotFoundError: No module named '_pyoz'in 0.12).pyoz publishonly uploads wheels for the current project version.- Wheel names are normalized (
My-Pkg→my_pkg-...whl) and RECORD files carry SHA-256 hashes, as the wheel spec requires. __freelist__is ignored on free-threaded Python. Regular builds are unchanged.pyoz buildmakes portable wheels. They are built for a baseline CPU, glibc 2.17 on Linux and macOS 13.0, and tagged from the built binary:manylinux_2_17_x86_64instead oflinux_x86_64, which PyPI rejected, andmacosx_13_0_arm64instead of the build machine's version (0.12 produced tags likemacosx_14_5_arm64, which pip never installs).--nativebuilds for the exact machine as before.linux-platform-tagnow selects the glibc version (manylinux_2_28_x86_64builds against glibc 2.28); alinux_*value means a native build. See pyoz build.- Wheel metadata is complete. Classifiers, license (SPDX expressions and
license files), readme, URLs, authors, keywords, dependencies, extras and
entry points from
[project]now reach the wheel and PyPI; 0.12 dropped everything but the name, version, summary andREADME.md. Dependencies declared inpyproject.tomlare now installed with the wheel. - Windows non-ABI3 builds link
python3XY.libinstead ofpython3.lib, which only exports the Stable ABI. - Fixed-size array parameters (
[3]i64) accept a tuple as well as a list. A wrong length now raisesValueError: expected 3 items, got 2(it was aTypeErrorwhose message was just the error name). pip install .from source needs no Zig installed: without a Zig 0.16 onPATH,pyoz.backendgets one from theziglangpackage.
New in 0.13 (optional)¶
- Async:
pyoz.asyncFnandpyoz.asyncMethod, built onstd.Io, with cancellation. - Free-threading:
.gil_used = falseto run without the GIL on 3.14t; class instances are locked automatically. pyoz.func(...).withParams("a, b"): real parameter names in stubs andhelp().- Cross-building wheels:
pyoz build --target all --python 3.13builds every platform from one machine. - Async protocols on classes:
__aiter__,__anext__,__await__,__aenter__,__aexit__(see Async).
See the CHANGELOG for the complete list.