Skip to content

WIP: PEP 688 demonstration#5665

Closed
blowekamp wants to merge 1 commit intoInsightSoftwareConsortium:mainfrom
blowekamp:pep_688
Closed

WIP: PEP 688 demonstration#5665
blowekamp wants to merge 1 commit intoInsightSoftwareConsortium:mainfrom
blowekamp:pep_688

Conversation

@blowekamp
Copy link
Copy Markdown
Member

Sharing code I wrote to play around with PEP 688 in ITK Python.

@blowekamp blowekamp requested a review from thewtex November 28, 2025 13:12
@github-actions github-actions bot added area:Python wrapping Python bindings for a class type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct area:Bridge Issues affecting the Bridge module area:Core Issues affecting the Core module area:Filtering Issues affecting the Filtering module labels Nov 28, 2025
@blowekamp
Copy link
Copy Markdown
Member Author

blowekamp commented Nov 28, 2025

@thewtex I don' have any intention of taking this change further. I am just sharing the code in case it's useful for exploring this new python feature in ITK Python. It has recently been implemented in SimpleITK.

This PR can be closed without merging.

Experiment with implementing PEP 688.

Replace __array__ with __buffer__. Convert memoryview to correctly
specified n-d view.

PEP 688 automatically maintains a a reference to the provider of the
buffer.

Add incomplete wrapping for the import image container, and buffer
interface.
@github-actions github-actions bot removed the area:Filtering Issues affecting the Filtering module label Nov 28, 2025
@hjmjohnson hjmjohnson marked this pull request as draft November 28, 2025 14:16
@hjmjohnson hjmjohnson changed the title PEP 688 demonstration WIP: PEP 688 demonstration Nov 28, 2025
@thewtex
Copy link
Copy Markdown
Member

thewtex commented Dec 5, 2025

@blowekamp wow! Awesome! Thank you so much for contributing this!

I'll push it forward

@blowekamp
Copy link
Copy Markdown
Member Author

There may additional useful information related to this here: SimpleITK/SimpleITK#2447

@hjmjohnson
Copy link
Copy Markdown
Member

Superseded by #6020 which builds on this exploration. The new PR adds PEP 688 __buffer__ and an opt-in __array_interface__ with full backward compatibility via itk.SIMULATE_PEP688.

hjmjohnson added a commit to hjmjohnson/ITK that referenced this pull request Apr 9, 2026
Add zero-copy data export to all wrapped itk.Image types via two
protocols, ensuring the exported array remains valid even after
the source image is deleted:

  image = itk.imread("brain.nii.gz")
  arr = np.asarray(image)
  del image
  print(arr[1,1,1])  # safe -- no crash

Protocol dispatch by Python version:
  3.12+:   np.asarray -> __buffer__ (PEP 688, zero-copy, memoryview
           pins self via NDArrayITKBase intermediary)
  3.10-11: np.asarray -> __array__ -> array_view_from_image (zero-copy,
           NDArrayITKBase.itk_base holds reference to image)

Changes to pyBase.i:
  - Add __buffer__() implementing PEP 688 buffer export with shaped
    memoryview. Uses NDArrayITKBase as intermediary to hold a Python
    reference to the image, preventing GC while any derived
    memoryview/array exists.
  - Simplify __array__() to always return zero-copy view via
    array_view_from_image(). Supports NumPy 2.0 copy= parameter.
  - Remove __array_interface__ (returned raw pointer with no reference
    holder -- use-after-free on del image, confirmed by test).
  - Remove SIMULATE_PEP688 / SIMULATE_PEP688_DEBUG (confusing,
    contradictory behaviors between __buffer__ and __array__ paths).

Changes to PyBuffer.i.init:
  - Add _get_buffer_formatstring() with module-level _BUFFER_FORMAT_MAP
    for struct format lookup (UC->B, SS->h, F->f, D->d, etc.)
  - Add _get_numpy_pixelid() with module-level _NUMPY_PIXELID_MAP
  - Remove LD (long double) mapping -- sizeof(long double) varies by
    platform, struct format "d" is always 8 bytes (silent corruption)

Supersedes InsightSoftwareConsortium#6020, InsightSoftwareConsortium#6018, InsightSoftwareConsortium#5673, InsightSoftwareConsortium#5665. Key improvements over each:
  - InsightSoftwareConsortium#6020: Fixed __buffer__ lifetime (memoryview didn't pin image),
    removed unsafe __array_interface__, removed SIMULATE_PEP688
  - InsightSoftwareConsortium#6018: Closed in favor of InsightSoftwareConsortium#6020
  - InsightSoftwareConsortium#5673: Added __array_interface__ (now removed) and __buffer__
  - InsightSoftwareConsortium#5665: Original PEP 688 implementation by blowekamp

Addresses review concerns from @thewtex (del image crash),
@blowekamp (reference pinning at buffer owner level).

Tested: 121 assertions across 3 test suites, Python 3.13 and 3.14,
with NumPy 2.4.3, PyTorch 2.11.0, Dask 2026.3.0. All 32 lifetime
tests pass (del image safe on every export path).

Co-Authored-By: Hans J. Johnson <hans-johnson@uiowa.edu>
hjmjohnson added a commit to hjmjohnson/ITK that referenced this pull request Apr 9, 2026
Add zero-copy data export to all wrapped itk.Image types via two
protocols, ensuring the exported array remains valid even after
the source image is deleted:

  image = itk.imread("brain.nii.gz")
  arr = np.asarray(image)
  del image
  print(arr[1,1,1])  # safe -- no crash

Protocol dispatch by Python version:
  3.12+:   np.asarray -> __buffer__ (PEP 688, zero-copy, memoryview
           pins self via NDArrayITKBase intermediary)
  3.10-11: np.asarray -> __array__ -> array_view_from_image (zero-copy,
           NDArrayITKBase.itk_base holds reference to image)

Changes to pyBase.i:
  - Add __buffer__() implementing PEP 688 buffer export with shaped
    memoryview. Uses NDArrayITKBase as intermediary to hold a Python
    reference to the image, preventing GC while any derived
    memoryview/array exists.
  - Simplify __array__() to always return zero-copy view via
    array_view_from_image(). Supports NumPy 2.0 copy= parameter.
  - Remove __array_interface__ (returned raw pointer with no reference
    holder -- use-after-free on del image, confirmed by test).
  - Remove SIMULATE_PEP688 / SIMULATE_PEP688_DEBUG (confusing,
    contradictory behaviors between __buffer__ and __array__ paths).

Changes to PyBuffer.i.init:
  - Add _get_buffer_formatstring() with module-level _BUFFER_FORMAT_MAP
    for struct format lookup (UC->B, SS->h, F->f, D->d, etc.)
  - Add _get_numpy_pixelid() with module-level _NUMPY_PIXELID_MAP
  - Remove LD (long double) mapping -- sizeof(long double) varies by
    platform, struct format "d" is always 8 bytes (silent corruption)

Supersedes InsightSoftwareConsortium#6020, InsightSoftwareConsortium#6018, InsightSoftwareConsortium#5673, InsightSoftwareConsortium#5665. Key improvements over each:
  - InsightSoftwareConsortium#6020: Fixed __buffer__ lifetime (memoryview didn't pin image),
    removed unsafe __array_interface__, removed SIMULATE_PEP688
  - InsightSoftwareConsortium#6018: Closed in favor of InsightSoftwareConsortium#6020
  - InsightSoftwareConsortium#5673: Added __array_interface__ (now removed) and __buffer__
  - InsightSoftwareConsortium#5665: Original PEP 688 implementation by blowekamp

Addresses review concerns from @thewtex (del image crash),
@blowekamp (reference pinning at buffer owner level).

Tested: 121 assertions across 3 test suites, Python 3.13 and 3.14,
with NumPy 2.4.3, PyTorch 2.11.0, Dask 2026.3.0. All 32 lifetime
tests pass (del image safe on every export path).

Co-Authored-By: Hans J. Johnson <hans-johnson@uiowa.edu>
hjmjohnson added a commit to hjmjohnson/ITK that referenced this pull request Apr 9, 2026
Add zero-copy data export to all wrapped itk.Image types via two
protocols, ensuring the exported array remains valid even after
the source image is deleted:

  image = itk.imread("brain.nii.gz")
  arr = np.asarray(image)
  del image
  print(arr[1,1,1])  # safe -- no crash

Protocol dispatch by Python version:
  3.12+:   np.asarray -> __buffer__ (PEP 688, zero-copy, memoryview
           pins self via NDArrayITKBase intermediary)
  3.10-11: np.asarray -> __array__ -> array_view_from_image (zero-copy,
           NDArrayITKBase.itk_base holds reference to image)

Changes to pyBase.i:
  - Add __buffer__() implementing PEP 688 buffer export with shaped
    memoryview. Uses NDArrayITKBase as intermediary to hold a Python
    reference to the image, preventing GC while any derived
    memoryview/array exists.
  - Simplify __array__() to always return zero-copy view via
    array_view_from_image(). Supports NumPy 2.0 copy= parameter.
    copy=True returns a plain ndarray (not NDArrayITKBase) so the
    image can be GCd immediately.
  - Remove __array_interface__ (returned raw pointer with no reference
    holder -- use-after-free on del image, confirmed by test).
  - Remove SIMULATE_PEP688 / SIMULATE_PEP688_DEBUG (confusing,
    contradictory behaviors between __buffer__ and __array__ paths).

Changes to PyBuffer.i.init:
  - Add _get_buffer_formatstring() with module-level _BUFFER_FORMAT_MAP
  - Add _get_numpy_pixelid() with module-level _NUMPY_PIXELID_MAP
  - Remove LD (long double) mapping (silent corruption)

Test suites added (121 assertions):
  itkImageTest.py (29): __buffer__, memoryview, np.asarray, __array__
  itkImageInteropTest.py (60): NumPy, PyTorch, Dask clinical sizes
  itkImageLifetimeTest.py (32): del image on every export path

Supersedes InsightSoftwareConsortium#6020, InsightSoftwareConsortium#6018, InsightSoftwareConsortium#5673, InsightSoftwareConsortium#5665.

Co-Authored-By: Hans J. Johnson <hans-johnson@uiowa.edu>
hjmjohnson added a commit to hjmjohnson/ITK that referenced this pull request Apr 9, 2026
Add zero-copy data export to all wrapped itk.Image types via two
protocols, ensuring the exported array remains valid even after
the source image is deleted:

  image = itk.imread("brain.nii.gz")
  arr = np.asarray(image)
  del image
  print(arr[1,1,1])  # safe -- no crash

Protocol dispatch by Python version:
  3.12+:   np.asarray -> __buffer__ (PEP 688, zero-copy, memoryview
           pins self via NDArrayITKBase intermediary)
  3.10-11: np.asarray -> __array__ -> array_view_from_image (zero-copy,
           NDArrayITKBase.itk_base holds reference to image)

Changes to pyBase.i:
  - Add __buffer__() implementing PEP 688 buffer export with shaped
    memoryview. Uses NDArrayITKBase as intermediary to hold a Python
    reference to the image, preventing GC while any derived
    memoryview/array exists.
  - Simplify __array__() to always return zero-copy view via
    array_view_from_image(). Supports NumPy 2.0 copy= parameter.
    copy=True returns a plain ndarray (not NDArrayITKBase) so the
    image can be GCd immediately.
  - Remove __array_interface__ (returned raw pointer with no reference
    holder -- use-after-free on del image, confirmed by test).
  - Remove SIMULATE_PEP688 / SIMULATE_PEP688_DEBUG (confusing,
    contradictory behaviors between __buffer__ and __array__ paths).

Changes to PyBuffer.i.init:
  - Add _get_buffer_formatstring() with module-level _BUFFER_FORMAT_MAP
  - Add _get_numpy_pixelid() with module-level _NUMPY_PIXELID_MAP
  - Remove LD (long double) mapping (silent corruption)

Test suites (145 assertions total, 0 failures on Python 3.13 + 3.14):
  itkImageTest.py (29): __buffer__, memoryview, np.asarray, __array__
  itkImageInteropTest.py (60): NumPy, PyTorch 2.11, Dask 2026.3
  itkImageLifetimeTest.py (56): lifetime safety on every export path,
    weak-ref GC verification, refcount checks, RSS growth detection,
    circular reference detection -- all inside a function scope with
    explicit cleanup to verify no memory leaks

Supersedes InsightSoftwareConsortium#6020, InsightSoftwareConsortium#6018, InsightSoftwareConsortium#5673, InsightSoftwareConsortium#5665. Key improvements:
  - InsightSoftwareConsortium#6020: Fixed __buffer__ lifetime, removed unsafe __array_interface__
  - InsightSoftwareConsortium#5665/InsightSoftwareConsortium#5673: Original PEP 688 by blowekamp, __array_interface__
Addresses review from @thewtex (del image crash), @blowekamp (ref pinning).

Co-Authored-By: Hans J. Johnson <hans-johnson@uiowa.edu>
hjmjohnson added a commit to hjmjohnson/ITK that referenced this pull request Apr 9, 2026
Add zero-copy data export to all wrapped itk.Image types via two
protocols, ensuring the exported array remains valid even after
the source image is deleted:

  image = itk.imread("brain.nii.gz")
  arr = np.asarray(image)
  del image
  print(arr[1,1,1])  # safe -- no crash

Protocol dispatch by Python version:
  3.12+:   np.asarray -> __buffer__ (PEP 688, zero-copy, memoryview
           pins self via NDArrayITKBase intermediary)
  3.10-11: np.asarray -> __array__ -> array_view_from_image (zero-copy,
           NDArrayITKBase.itk_base holds reference to image)

Changes to pyBase.i:
  - Add __buffer__() implementing PEP 688 buffer export with shaped
    memoryview. Uses NDArrayITKBase as intermediary to hold a Python
    reference to the image, preventing GC while any derived
    memoryview/array exists.
  - Simplify __array__() to always return zero-copy view via
    array_view_from_image(). Supports NumPy 2.0 copy= parameter.
    copy=True returns a plain ndarray (not NDArrayITKBase) so the
    image can be GCd immediately.
  - Remove __array_interface__ (returned raw pointer with no reference
    holder -- use-after-free on del image, confirmed by test).
  - Remove SIMULATE_PEP688 / SIMULATE_PEP688_DEBUG (confusing,
    contradictory behaviors between __buffer__ and __array__ paths).

Changes to PyBuffer.i.init:
  - Add _get_buffer_formatstring() with module-level _BUFFER_FORMAT_MAP
  - Add _get_numpy_pixelid() with module-level _NUMPY_PIXELID_MAP
  - Remove LD (long double) mapping (silent corruption)

Test suites added (121 assertions):
  itkImageTest.py (29): __buffer__, memoryview, np.asarray, __array__
  itkImageInteropTest.py (60): NumPy, PyTorch, Dask clinical sizes
  itkImageLifetimeTest.py (32): del image on every export path

Supersedes InsightSoftwareConsortium#6020, InsightSoftwareConsortium#6018, InsightSoftwareConsortium#5673, InsightSoftwareConsortium#5665.

Co-Authored-By: Hans J. Johnson <hans-johnson@uiowa.edu>
hjmjohnson added a commit to hjmjohnson/ITK that referenced this pull request Apr 9, 2026
Add zero-copy data export to all wrapped itk.Image types via two
protocols, ensuring the exported array remains valid even after
the source image is deleted:

  image = itk.imread("brain.nii.gz")
  arr = np.asarray(image)
  del image
  print(arr[1,1,1])  # safe -- no crash

Protocol dispatch by Python version:
  3.12+:   np.asarray -> __buffer__ (PEP 688, zero-copy, memoryview
           pins self via NDArrayITKBase intermediary)
  3.10-11: np.asarray -> __array__ -> array_view_from_image (zero-copy,
           NDArrayITKBase.itk_base holds reference to image)

Changes to pyBase.i:
  - Add __buffer__() implementing PEP 688 buffer export with shaped
    memoryview. Uses NDArrayITKBase as intermediary to hold a Python
    reference to the image, preventing GC while any derived
    memoryview/array exists.
  - Simplify __array__() to always return zero-copy view via
    array_view_from_image(). Supports NumPy 2.0 copy= parameter.
    copy=True returns a plain ndarray (not NDArrayITKBase) so the
    image can be GCd immediately.
  - Remove __array_interface__ (returned raw pointer with no reference
    holder -- use-after-free on del image, confirmed by test).
  - Remove SIMULATE_PEP688 / SIMULATE_PEP688_DEBUG (confusing,
    contradictory behaviors between __buffer__ and __array__ paths).

Changes to PyBuffer.i.init:
  - Add _get_buffer_formatstring() with module-level _BUFFER_FORMAT_MAP
  - Add _get_numpy_pixelid() with module-level _NUMPY_PIXELID_MAP
  - Remove LD (long double) mapping (silent corruption)

Test suites added (121 assertions):
  itkImageTest.py (29): __buffer__, memoryview, np.asarray, __array__
  itkImageInteropTest.py (60): NumPy, PyTorch, Dask clinical sizes
  itkImageLifetimeTest.py (32): del image on every export path

Supersedes InsightSoftwareConsortium#6020, InsightSoftwareConsortium#6018, InsightSoftwareConsortium#5673, InsightSoftwareConsortium#5665.

Co-Authored-By: Hans J. Johnson <hans-johnson@uiowa.edu>
@hjmjohnson hjmjohnson closed this Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Bridge Issues affecting the Bridge module area:Core Issues affecting the Core module area:Python wrapping Python bindings for a class type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants