Unit 7.8 · about 5 h
49 / 324Python and native code: dtype, stride, ownership and GIL contracts
An array's Python type does not specify its memory contract. The extension must agree with Python about representation, lifetime and errors before it can safely compute on raw storage.
- explain
You can now
- Explain a native function's dtype, byte order, rank, shape, stride and alignment requirements.
- Distinguish copying input or output from borrowing storage, and identify which object keeps borrowed storage alive.
- Explain exception translation and which parts of a call may run without the GIL on default CPython.
Covers
- Python/native dtype, shape, stride, ownership, GIL and error contracts
Study, in this order
5 steps
- First stepspybind11 documentation
Header and namespace conventions; Creating bindings for a simple function; Compiling the test cases; Keyword arguments
Depth · Use pybind11 3.1 and your Python 3.14 development headers; skip exporting variables and unrelated C++11 syntax repair.
- NumPy and buffer contractspybind11 documentation
Buffer protocol; Arrays; Direct access; Memory view
Depth · Skip Eigen and vectorizing functions. Do not copy the
Matrixownership sketch or theadd_arraysraw-pointer loop unchanged:Matrixomits cleanup, andadd_arraysaccepts strided input but indexes it as contiguous. Use RAII storage and either validated contiguous input or stride-aware access. - Conversion and return policiespybind11 documentation
Return value policies; Additional call policies: Keep alive; Non-converting arguments
Depth · Read ownership policies, not the crash wording as a guaranteed outcome of undefined behavior.
- GIL and free-threading contractspybind11 documentation
Global Interpreter Lock (GIL); Common Sources Of Global Interpreter Lock Errors; Free-threading support
Depth · Apply GIL-held wording to the default build. Free-threaded Python was introduced in 3.13 and became supported in 3.14; the page's experimental wording is not the current CPython 3.14 status. Stop before sub-interpreter implementation.
- Exception translationpybind11 documentation
Built-in C++ to Python exception translation table; Handling exceptions from Python in C++
Depth · Skip custom translators and unraisable exceptions for this first binding.
Focus on
py::array_t<double>permits conversion by default; it does not mean no-copy or contiguous. For a strict 1-D native-endianfloat64contract, usepy::array_t<double, py::array::c_style>withpy::arg(...).noconvert(), check rank and alignment, and reject unsupported input explicitly.- Buffer strides are signed byte distances. A reversed view can have a negative stride; pointer indexing is valid only when the accepted layout supports it.
unchecked<N>handles strides but trusts your index bounds and the storage's continued validity. - Returning newly allocated NumPy-owned storage makes ownership explicit. A borrowed pointer instead needs an owner that stays alive and a no-reallocation contract; a reference return policy alone does not make dangling memory safe.
- On default CPython 3.14, pybind11 does not implicitly release the GIL. Validate and acquire views while holding it, release only around native work, and reacquire before Python operations or reference-count changes.
mod_gil_not_usedpromises thread safety; it does not provide it.
Reproduce closed-book
-std=c++20 -O0 -g, bind a 1-D float64 sum using py::array_t<double, py::array::c_style> and .noconvert(). Reject ndim != 1 with py::value_error and a data pointer not aligned to alignof(double) with py::value_error; obtain unchecked<1>() before a scoped GIL release around the sum loop. Keep the input object alive and require no concurrent mutation. np.arange(6, dtype=np.float64) returns 15.0 and empty float64 input returns 0.0. float32 input, a list, a[::2] and a[::-1] raise TypeError; a.reshape(2,3) raises ValueError. Return a separately allocated py::array_t<double> copy in a second function, and test that it does not share memory with the input and survives deletion of the input. Also reject an unaligned contiguous float64 array constructed over a byte buffer with offset 1. Explain the owner and GIL state at each point rather than claiming a speedup.Traps
- Releasing the GIL does not prevent another caller from mutating the same NumPy storage. Require exclusive access or copy before release.
- A C-contiguous array is not necessarily aligned. Check alignment before dereferencing it as
doublestorage. - C++ exceptions can translate to Python exceptions; memory corruption and undefined behavior cannot reliably be contained that way.
Practice
The section mini-project covers this: expose the native function with dtype, shape, stride, alignment, ownership, GIL and exception contracts, and test accepted and rejected arrays plus output lifetime.
Self-test
-
Does
py::array_t<double>by itself guarantee a zero-copy contiguousfloat64argument?No. Its defaultforcecastcan convert input, and it permits strided arrays. Specify and test the layout and conversion policy. -
Why is
a[::2]unsafe for an unchecked rawdouble*loop usingptr[i]?The logical stride skips storage elements, butptr[i]advances by onedouble. Use actual strides or reject that layout. -
What keeps a returned borrowed view valid?
A live owner of the backing allocation and a contract that prevents its deletion, resizing or reallocation while the view is used. A raw address alone provides neither. -
May a
gil_scoped_releaseregion create or destroy Python wrapper objects on the default build?Not safely without reacquiring the GIL for those operations. Their constructors, destructors or copies can access Python state and reference counts. -
What does declaring
mod_gil_not_usedmean for a free-threaded build?The extension promises that its code is thread-safe without the GIL. It must be built for that interpreter; the tag supplies no synchronization for shared user data.
Completion criterion
Stop when
Your strict-binding and independent-output tests pass, and you can explain each accepted or rejected input, the live storage owner and every GIL transition.
Mark passed, then open 7.9 Only mark this when you can do it without notes.Leads to
- 7.9 Repeatable CMake builds and test targets Build and test the binding against the intended interpreter.
Comes after
- 7.1 Compiling, linking, loading, ABI and C linkageApply compatibility and ownership contracts at import and call time.
- 7.3 Struct layout, alignment, strides and localityValidate dtype, shape and byte strides at the extension boundary.
- 7.4 Storage duration, RAII, smart pointers and move semanticsDecide whether Python borrows, copies or owns returned storage.
- 7.6 Undefined behavior, sanitizers and debugger evidenceDiagnose native failures without relying on Python exceptions to contain them.
- 7.7 Threads, data races, memory ordering and atomicsReason about native shared state when the GIL is released or absent.
Resources for this unit
- pybind11 documentation Follow First steps and NumPy support to expose one small native function with explicit shape, dtype, stride, ownership, GIL and exception behavior.
Also in this section
- AlgoMaster C++ Use the sections on compilation, pointers and references, memory, RAII and smart pointers, copy and move, STL containers, templates, errors, concurrency and paired exercises. Skip competitive-programming breadth already covered in Algorithms.
- C++ Core Guidelines Use the Philosophy, Interfaces, Resource management, Expressions, Containers and concurrency rule groups to review ownership and safety decisions in the native component.
- cppreference Consult language lifetime, object model, value categories, memory model, atomics, containers and iterator invalidation while reading or modifying code.
- Clang AddressSanitizer and UndefinedBehaviorSanitizer Compile seeded memory defects with AddressSanitizer and UndefinedBehaviorSanitizer, preserve the reports, repair the root cause and keep regression tests.
- CMake tutorial Use the introductory steps for targets, libraries, testing and install rules. Produce one clean debug build and one optimized build without copied one-off commands.
- Algorithmica - AoS and SoA Read the AoS and SoA experiment for a measured case where keeping each record together wins, the counterpart to a single-field scan.
- `ld.so`: dynamic linker/loader Read DESCRIPTION, Dynamic string tokens and
LD_DEBUGto distinguish link-time symbols from runtime shared-library discovery on Linux. - Memory Layout Transformations Read Overview and Topics through AOS Form and SOA Form for field-wise access and locality trade-offs. Skip Intel array-notation syntax and the LIBOR example.
- GDB manual: Backtraces Use section 8.2 for frame numbers,
bt,bt -fulland optimized-out arguments when inspecting a native failure.