# Looking for guidance on writing python unit tests using python vtk

**URL:** https://discourse.vtk.org/t/looking-for-guidance-on-writing-python-unit-tests-using-python-vtk/16148
**Category:** Development
**Tags:** python
**Created:** [November 17, 2025, 9:24pm UTC](https://discourse.vtk.org/t/looking-for-guidance-on-writing-python-unit-tests-using-python-vtk/16148 "2025-11-17T21:24:08Z")
**Posts on this page:** 9
**Page:** 1

<div class="post-metadata">

### Author: ![Dan\_The\_Man](https://discourse.vtk.org/user_avatar/discourse.vtk.org/dan_the_man/32/10263_2.png) [@Dan\_The\_Man](https://discourse.vtk.org/u/Dan_The_Man)
#### Post date: [November 17, 2025, 9:24pm UTC](https://discourse.vtk.org/t/looking-for-guidance-on-writing-python-unit-tests-using-python-vtk/16148/1 "2025-11-17T21:24:08Z")

</div>

I am developing a python-VTK application and am noticing some unusual behavior in python `unittest`-based tests I am building alongside my application. I have noticed that tests that are defined in separate suites appear to interfere with each other and my theory is that the underlying python VTK functionality is using some global or otherwise shared information making the scoping of my python tests not as isolated as I’d like. Ultimately I’m looking for tips/guidance on how to correctly set up python unit tests that leverage python VTK in a safe and isolated manner.

I have a couple anecdotal examples, first I noticed that unit tests which pass when run once will sometimes fail if running them multiple times per python interpreter. I have found that after a dozen or so executions of the same test, I’d receive a runtime failure that looks like this (python VTK 9.5.1 on Mac):

```auto

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x726f77656d61726e)
    frame #0: 0x000000010777f150 libvtkCommonCore.dylib`vtkSubjectHelper::InvokeEvent(unsigned long, void*, vtkObject*) + 44
libvtkCommonCore.dylib`vtkSubjectHelper::InvokeEvent:
-> 0x10777f150 <+44>: ldp x9, x8, [x0, #0x8]
    0x10777f154 <+48>: cmp x9, x8, lsl #6
    0x10777f158 <+52>: b.ne 0x10777f194 ; <+112>
    0x10777f15c <+56>: cmn x9, #0x1
Target 0: (Python) stopped.

```

Through much testing I discovered that this error can be avoided 100% of the time by turning off python garbage collection with `import gc; gc.disable()` at the top of my testing script. The effectiveness of this approach has led me to believe that there is something unsafe happening when the python script creates multiple full rendering pipelines and those python objects call underlying C++ libraries to deconstruct. Telling python to never clean up bypasses the issue.

Another example - I have two suites, one of which uses `self.render_window.OffScreenRenderingOn()` and the other does not. If both tests are run in the same python instance, I notice the offscreen rendering test actually renders to the screen when it shouldn’t. Only when both tests are in the same file will this occur.

I can work on getting a standalone example of this to share but in the meantime I’m curious if anyone out there has run into this or has any tips on isolation and memory safety when running python VTK based unit tests. Thanks!

---

<div class="post-metadata">

### Author: ![dgobbi](https://discourse.vtk.org/user_avatar/discourse.vtk.org/dgobbi/32/18_2.png) [@dgobbi](https://discourse.vtk.org/u/dgobbi)
#### Post date: [November 17, 2025, 10:08pm UTC](https://discourse.vtk.org/t/looking-for-guidance-on-writing-python-unit-tests-using-python-vtk/16148/2 "2025-11-17T22:08:36Z")

</div>

For the runtime failures, do the stack traces always show `InvokeEvent`? I suspect that events are being invoked after the observer has destructed. This would explain why disabling gc helps. If you can confirm that `InvokeEvent` is always involved, I can dig deeper to find the cause.

Regarding the offscreen rendering, I’m not very familiar with that part of the VTK code, but I can imagine that it might involve some global state. VTK’s own test suite always runs each test in its own process.

---

<div class="post-metadata">

### Author: ![Dan\_The\_Man](https://discourse.vtk.org/user_avatar/discourse.vtk.org/dan_the_man/32/10263_2.png) [@Dan\_The\_Man](https://discourse.vtk.org/u/Dan_The_Man)
#### Post date: [November 18, 2025, 1:17am UTC](https://discourse.vtk.org/t/looking-for-guidance-on-writing-python-unit-tests-using-python-vtk/16148/3 "2025-11-18T01:17:56Z")

</div>

Hi David - I really appreciate your help. Yes when I see this `EXC_BAD_ACCESS` it’s always a stack trace that looks like this:

```auto
  * frame #0: 0x000000011129b1b8 libvtkCommonCore.dylib`vtkSubjectHelper::InvokeEvent(unsigned long, void*, vtkObject*) + 148
    frame #1: 0x0000000199556fcc Foundation`__NSFireTimer + 104
    frame #2: 0x0000000197f67ba0 CoreFoundation` __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 32
    frame #3: 0x0000000197f67860 CoreFoundation`__CFRunLoopDoTimer + 980
    frame #4: 0x0000000197f6739c CoreFoundation`__CFRunLoopDoTimers + 332
    frame #5: 0x0000000197f4d7a8 CoreFoundation`__CFRunLoopRun + 1848
    frame #6: 0x0000000197f4c9e8 CoreFoundation`CFRunLoopRunSpecific + 572
    frame #7: 0x000000019951cc78 Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 212
    frame #8: 0x00000001995903a4 Foundation`-[NSRunLoop(NSRunLoop) runUntilDate:] + 100
    frame #9: 0x000000011752b89c libvtkRenderingOpenGL2.dylib`vtkCocoaRenderWindow::Render() + 124
    frame #10: 0x0000000116f5fea4 vtkRenderingOpenGL2.cpython-311-darwin.so`PyvtkCocoaRenderWindow_Render(_object*, _object*) + 244
    frame #11: 0x000000010069ea18 Python`cfunction_call + 100
    frame #12: 0x0000000100654140 Python`_PyObject_MakeTpCall + 128
    frame #13: 0x00000001007302d8 Python`_PyEval_EvalFrameDefault + 41220
    frame #14: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #15: 0x00000001006575b4 Python`method_vectorcall + 168
    frame #16: 0x0000000100731f64 Python`_PyEval_EvalFrameDefault + 48528
    frame #17: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #18: 0x00000001006543e8 Python`_PyObject_FastCallDictTstate + 96
    frame #19: 0x00000001006bcc1c Python`slot_tp_call + 188
    frame #20: 0x0000000100654140 Python`_PyObject_MakeTpCall + 128
    frame #21: 0x00000001007302d8 Python`_PyEval_EvalFrameDefault + 41220
    frame #22: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #23: 0x00000001006575b4 Python`method_vectorcall + 168
    frame #24: 0x0000000100731f64 Python`_PyEval_EvalFrameDefault + 48528
    frame #25: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #26: 0x00000001006543e8 Python`_PyObject_FastCallDictTstate + 96
    frame #27: 0x00000001006bcc1c Python`slot_tp_call + 188
    frame #28: 0x0000000100654140 Python`_PyObject_MakeTpCall + 128
    frame #29: 0x00000001007302d8 Python`_PyEval_EvalFrameDefault + 41220
    frame #30: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #31: 0x00000001006575b4 Python`method_vectorcall + 168
    frame #32: 0x0000000100731f64 Python`_PyEval_EvalFrameDefault + 48528
    frame #33: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #34: 0x00000001006543e8 Python`_PyObject_FastCallDictTstate + 96
    frame #35: 0x00000001006bcc1c Python`slot_tp_call + 188
    frame #36: 0x0000000100654140 Python`_PyObject_MakeTpCall + 128
    frame #37: 0x00000001007302d8 Python`_PyEval_EvalFrameDefault + 41220
    frame #38: 0x0000000100725608 Python`PyEval_EvalCode + 168
    frame #39: 0x000000010077beac Python`run_eval_code_obj + 84
    frame #40: 0x000000010077be10 Python`run_mod + 112
    frame #41: 0x000000010077bc50 Python`pyrun_file + 148
    frame #42: 0x000000010077b6a0 Python`_PyRun_SimpleFileObject + 268
    frame #43: 0x000000010077b02c Python`_PyRun_AnyFileObject + 216
    frame #44: 0x0000000100797a9c Python`pymain_run_file_obj + 220
    frame #45: 0x00000001007973e0 Python`pymain_run_file + 72
    frame #46: 0x0000000100796cac Python`Py_RunMain + 704
    frame #47: 0x0000000100797dec Python`Py_BytesMain + 40
    frame #48: 0x0000000197ac2b98 dyld`start + 6076

```

When I first ran into this issue I ran it by a couple AIs which advised me to better manage tearing down VTK-managed objects myself. This led me to create a `tear_down` method that attempts to deconstruct everything I use in my top level python class, it looks like this:

```python
    def tear_down(self):
        if self.controller.timer_id:
            self.interactor.DestroyTimer(self.controller.timer_id)
        for camera in self.controller.cameras:
            self.controller.cameras[camera].RemoveAllObservers()

        self.interactor.RemoveAllObservers()

        for renderer in self.renderers:
            self.renderers[renderer].RemoveAllObservers()

        # Mark for deletion
        del self.interactor
        self.interactor = None
        for renderer in self.renderers:
            del renderer

```

Calling the function however appeared to make the problem _worse_ and would result in another type of error signature (in addition to the first one) that looks like this:

```auto
(lldb) thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x10000fe0028)
  * frame #0: 0x0000000106e7ff50 libvtkCommonCore.dylib`vtkObject::RemoveObserver(vtkCommand*) + 40
    frame #1: 0x0000000102e1e5a0 libvtkRenderingCore.dylib`vtkInteractorStyle::SetInteractor(vtkRenderWindowInteractor*) + 44
    frame #2: 0x0000000102e1e278 libvtkRenderingCore.dylib`vtkInteractorStyle::~vtkInteractorStyle() + 36
    frame #3: 0x000000010a81975c libvtkInteractionStyle.dylib`vtkInteractorStyleTrackballCamera::~vtkInteractorStyleTrackballCamera() + 12
    frame #4: 0x00000001014bce38 libvtkWrappingPythonCore3.11.dylib`vtkPythonUtil::~vtkPythonUtil() + 320
    frame #5: 0x00000001014bcb70 libvtkWrappingPythonCore3.11.dylib`vtkPythonUtilDelete + 28
    frame #6: 0x0000000100776a68 Python`Py_FinalizeEx + 1076
    frame #7: 0x0000000100796afc Python`Py_RunMain + 272
    frame #8: 0x0000000100797dec Python`Py_BytesMain + 40
    frame #9: 0x0000000197ac2b98 dyld`start + 6076

```

Backing this out seemed to completely remove this error signature - from this I concluded that 1. I clearly don’t know the safe way to deconstruct VTK objects in python and 2. AI will not be taking over the world with software anytime soon 😛

> VTK’s own test suite always runs each test in its own process.

Thanks for this info - I could do as well on the python side if I absolutely had to.

---

<div class="post-metadata">

### Author: ![dgobbi](https://discourse.vtk.org/user_avatar/discourse.vtk.org/dgobbi/32/18_2.png) [@dgobbi](https://discourse.vtk.org/u/dgobbi)
#### Post date: [November 18, 2025, 7:50pm UTC](https://discourse.vtk.org/t/looking-for-guidance-on-writing-python-unit-tests-using-python-vtk/16148/4 "2025-11-18T19:50:08Z")

</div>

The tear-down should not be necessary. In any case, the `del` loop in the following code makes no sense, and deleting an attribute before assigning it doesn’t serve any purpose either.

> [@](#):
>
> ```auto
> # Mark for deletion
> del self.interactor
> self.interactor = None
> for renderer in self.renderers:
> del renderer
> 
> ```

Now that I know it’s a timer event, that narrows down where I have to look for the bug. Can you give any hints on what kind of observer it is? Mainly, I’m wondering if the AddObserver was called from Python, or if it was called from within VTK itself.

---

<div class="post-metadata">

### Author: ![Dan\_The\_Man](https://discourse.vtk.org/user_avatar/discourse.vtk.org/dan_the_man/32/10263_2.png) [@Dan\_The\_Man](https://discourse.vtk.org/u/Dan_The_Man)
#### Post date: [November 18, 2025, 9:46pm UTC](https://discourse.vtk.org/t/looking-for-guidance-on-writing-python-unit-tests-using-python-vtk/16148/5 "2025-11-18T21:46:39Z")

</div>

If it helps here’s a `git grep AddObserver` for my project:

```auto
Virgo.py: self.AddObserver("CharEvent", self.onChar)
Virgo.py: self.AddObserver(vtk.vtkCommand.EndInteractionEvent, self.StoreRelativeCameraInfo, 0.0)
Virgo.py: self.cameras['foreground'].AddObserver("ModifiedEvent", self.sync_cameras)
Virgo.py: self.interactor.AddObserver("RightButtonPressEvent", self.on_right_click)
Virgo.py: self.interactor.AddObserver("TimerEvent", self.on_timer)
Virgo.py: self.interactor.AddObserver("KeyPressEvent", self.on_key_press)
VirgoSplash.py: self.interactor.AddObserver("TimerEvent", lambda obj,event: (

```

Both of these files are included here in case seeing the context around it would be helpful.

[Virgo.py](https://discourse.vtk.org/uploads/short-url/g02nRA3Nyywu2gnJnGYztrsKfcd.py) (67.9 KB)

[VirgoSplash.py](https://discourse.vtk.org/uploads/short-url/gMoFavxoqp0saaOIZZua5Y04jSp.py) (3.7 KB)

This project is intended to be open source under [NASA · GitHub](https://github.com/nasa/) but I’m running into some obstacles in the official software release process which is slowing it down. That said I can probably hack something together in a temporary area in GitHub to provide a state of code where I can reliably replicate this - let me know if that would be useful for you in your efforts. Thanks!

---

<div class="post-metadata">

### Author: ![dgobbi](https://discourse.vtk.org/user_avatar/discourse.vtk.org/dgobbi/32/18_2.png) [@dgobbi](https://discourse.vtk.org/u/dgobbi)
#### Post date: [November 19, 2025, 1:44am UTC](https://discourse.vtk.org/t/looking-for-guidance-on-writing-python-unit-tests-using-python-vtk/16148/6 "2025-11-19T01:44:26Z")

</div>

I’ve made a reproducer, it’s not minimal but it’s something that I can use as a starting point for debugging. Interestingly, it only crashes on macos, I haven’t been able to get it to crash on linux.

See the code in the expandable section below. Closing the window that it creates will sporadically cause a crash, causing a stack trace like the one you posted above. And disabling Python gc stops the crash.

> ****
>
> ```auto
> from vtkmodules.vtkFiltersSources import (
> vtkCylinderSource,
> )
> from vtkmodules.vtkRenderingCore import (
> vtkActor,
> vtkCamera,
> vtkPolyDataMapper,
> vtkProperty,
> vtkRenderer,
> vtkRenderWindow,
> vtkRenderWindowInteractor,
> )
> import vtkmodules.vtkRenderingOpenGL2
> import vtkmodules.vtkInteractionStyle
> import vtkmodules.vtkRenderingUI
> 
> src = vtkCylinderSource()
> 
> mapper = vtkPolyDataMapper()
> mapper.SetInputConnection(src.GetOutputPort())
> 
> actor = vtkActor()
> actor.SetMapper(mapper)
> 
> actor.GetProperty().EdgeVisibilityOn()
> actor.GetProperty().SetLineWidth(5)
> actor.GetProperty().SetOpacity(0.5)
> 
> ren = vtkRenderer()
> ren.AddActor(actor)
> ren.SetBackground(0.1, 0.2, 0.4)
> 
> renWin = vtkRenderWindow()
> renWin.AddRenderer(ren)
> renWin.SetSize(500, 500)
> 
> iren = vtkRenderWindowInteractor()
> iren.SetRenderWindow(renWin)
> iren.Initialize()
> 
> def on_timer(caller, event):
> caller.Render()
> ren.GetActiveCamera().Azimuth(1)
> 
> iren.AddObserver("TimerEvent", on_timer)
> 
> timer_id = iren.CreateRepeatingTimer(20)
> 
> iren.Start()
> 
> # repeating the code causes sporadic crashes on macos
> 
> src = vtkCylinderSource()
> 
> mapper = vtkPolyDataMapper()
> mapper.SetInputConnection(src.GetOutputPort())
> 
> actor = vtkActor()
> actor.SetMapper(mapper)
> 
> actor.GetProperty().EdgeVisibilityOn()
> actor.GetProperty().SetLineWidth(5)
> actor.GetProperty().SetOpacity(0.5)
> 
> ren = vtkRenderer()
> ren.AddActor(actor)
> ren.SetBackground(0.1, 0.2, 0.4)
> 
> renWin = vtkRenderWindow()
> renWin.AddRenderer(ren)
> renWin.SetSize(500, 500)
> 
> iren = vtkRenderWindowInteractor()
> iren.SetRenderWindow(renWin)
> iren.Initialize()
> 
> iren.AddObserver("TimerEvent", on_timer)
> 
> timer_id = iren.CreateRepeatingTimer(20)
> 
> iren.Start()
> 
> ```

Edit: I’ve found a fix that I can merge before the VTK 9.6 release. For now, you can work-around by ensuring that you call `interactor.DestroyTimer(timerId)` for every timer that you create before the interactor goes out of scope and destructs. Don’t do `RemoveAllObservers()` because VTK sometimes adds its own observers for its own purposes.

---

<div class="post-metadata">

### Author: ![Dan\_The\_Man](https://discourse.vtk.org/user_avatar/discourse.vtk.org/dan_the_man/32/10263_2.png) [@Dan\_The\_Man](https://discourse.vtk.org/u/Dan_The_Man)
#### Post date: [November 19, 2025, 6:02pm UTC](https://discourse.vtk.org/t/looking-for-guidance-on-writing-python-unit-tests-using-python-vtk/16148/7 "2025-11-19T18:02:22Z")

</div>

Thanks so much for the guidance I’ll give it a shot!

---

<div class="post-metadata">

### Author: ![Dan\_The\_Man](https://discourse.vtk.org/user_avatar/discourse.vtk.org/dan_the_man/32/10263_2.png) [@Dan\_The\_Man](https://discourse.vtk.org/u/Dan_The_Man)
#### Post date: [November 24, 2025, 4:52pm UTC](https://discourse.vtk.org/t/looking-for-guidance-on-writing-python-unit-tests-using-python-vtk/16148/8 "2025-11-24T16:52:28Z")

</div>

Update - I backed out `gc.disable()` and added just `interactor.DestroyTimer(timerId)` as a “tear down” mechanism for my top level class - this definitely helped as I struggled to replicate the `InvokeEvent` error for quite some time. I will note that after 116 retries of the test I did get a single segmentation fault but couldn’t capture the stack trace as it wasn’t instrumented during that setup. I ended up moving forward turning all my unit tests back on and by keeping `gc.disable()` at the top of my test I have now executed a few thousand tests without a single issue wrt memory.

So there may still be an issue but it’s much more rare with your suggestions implemented and completely avoidable with python garbage collection disabled. I’ll be happy to re-test when the newest VTK release is available and provide more detail then if I can still reproduce the error.

Thanks David this really helped push my project in the forward direction!

---

<div class="post-metadata">

### Author: ![dgobbi](https://discourse.vtk.org/user_avatar/discourse.vtk.org/dgobbi/32/18_2.png) [@dgobbi](https://discourse.vtk.org/u/dgobbi)
#### Post date: [November 24, 2025, 5:03pm UTC](https://discourse.vtk.org/t/looking-for-guidance-on-writing-python-unit-tests-using-python-vtk/16148/9 "2025-11-24T17:03:42Z")

</div>

That’s good to hear. If you’re curious about the fix, it’s [!12636](https://gitlab.kitware.com/vtk/vtk/-/merge_requests/12636/diffs) and it has been merged into the master branch.

It would be nice if you could eventually remove the `gc.disable()`, because if there are any remaining memory issues, I’d rather see them fixed, instead of hidden.
