Skip to content

[Bug]: Event handling with input in callback function #30419

Open

Description

@ljlamarche

Bug summary

When using a button click event handler, if the callback function includes the input function, the figure automatically closes when the callback function finishes.

Code for reproduction

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.random.rand(10))

def onclick(event):
    print(f'click: x={event.x}, y={event.y}')
    info = input('Enter some information about this point: ')  # Comment out this line
    print(info)                                                # Comment out this line

cid = fig.canvas.mpl_connect('button_press_event', onclick)

plt.show()

Actual outcome

After a single event and iteration of the onclick callback function, the figure automatically closes without giving the user the opportunity to click on more points.

Screen.Recording.2025-08-12.at.5.17.43.PM.mov

Expected outcome

The figure should remain open and allow the user to click on many points until the user explicitly closes it. This is the standard behavior when the two lines indicated in the MWE related to the input function are commented out.

Screen.Recording.2025-08-12.at.5.23.06.PM.mov

Additional information

No response

Operating system

OS/X

Matplotlib Version

3.10.3

Matplotlib Backend

macosx

Python version

Python 3.13.5

Jupyter version

4.4.5

Installation

pip

Activity

  1. QuLogic commented on Aug 13, 2025

    @QuLogic
    Member

    This works fine with Qt (other than an exception if you click twice before entering something, since you can't call input twice), so this seems to be a problem with the macosx backend specifically.

  2. QuLogic commented on Aug 13, 2025

    @QuLogic
    Member

    Maybe @greglucas has some thoughts.

  3. ljlamarche commented on Aug 13, 2025

    @ljlamarche
    Author

    Thank you for the tip! This does work as expected if I manually set a different backend. Adding this to the top of the script has served as an effective work-around.

    import matplotlib
    if matplotlib.get_backend() == 'macosx':
        matplotlib.use('tkagg')
    
  4. greglucas commented on Aug 15, 2025

    @greglucas
    Contributor

    I'm guessing this has something to do with our call to stopWithEvent() when we override the input stream, but I'm not certain.

    usingBlock: ^(NSNotification *notification) {stopWithEvent();}

    Maybe we need to have a different check on what came from stdin "if it was a sigint: stop entirely, otherwise pause/let go of control" or something like that.

    I did check back a few versions and this looks like it does work on 3.8.4, but not on 3.9.3, I didn't bisect it yet.

  5. dstansby commented on Aug 20, 2025

    @dstansby
    Member

    This bisects back to 18909c7

  6. iccir commented on Jul 5, 2026

    @iccir
    Contributor

    The problem is that we are using -[NSApplication run] from both wait_for_stdin() and show(). When we call -[NSApplication stop:] from stopWithEvent(), it will stop both -run calls.

    The documentation for -stop: is deceptive:

    If you call this method from an event handler running in your main run loop, the app object exits out of the run method, thereby returning control to the main() function. If you call this method from within a modal event loop, it will exit the modal loop instead of the main event loop.

    That seems to indicate that some degree of re-entrancy is supported, but that only applies to modal sessions.

    The implementation of -run and -stop are:

    Pseudocode for -run and -stop:
    - (void) run
    {
        _running = 1;
        
        if (!_calledFinishLaunching) {
            [self finishLaunching];
            _calledFinishLaunching = YES;
        }
    
        // A whole bunch of additional one-time initialization code
        
        while (_running) {
            NSEvent *event = [NSApp nextEventMatchingMask: NSEventMaskAny
                                                untilDate: [NSDate distantFuture]
                                                   inMode: NSDefaultRunLoopMode
                                                  dequeue: YES];
            
            [self sendEvent:event];
        }
    }
    
    - (void) stop:(id)sender
    {
        _running = 0;
    }

    Hence, once -stop: is called, all nested calls to -run will return since NSApplication's _running ivar is 0. Here's a vibe-coded example that demonstrates this:

    NestedRunTest.m
    #import <Cocoa/Cocoa.h>
    
    @interface AppDelegate : NSObject <NSApplicationDelegate> 
    @end
    
    
    @implementation AppDelegate {
        NSInteger _nestLevel;
    }
    
    
    - (void) applicationDidFinishLaunching:(NSNotification *)notification
    {
        NSLog(@"Outer run started, _nestLevel = %ld", (long)_nestLevel);
    
        [NSApp addObserver:self forKeyPath:@"running" options:NSKeyValueObservingOptionNew context:NULL];
    
        [self startEventPump];
    
        [self performSelector:@selector(nestRun) withObject:nil afterDelay:0.05];
    }
    
    
    - (void) startEventPump
    {
        NSTimer *timer = [NSTimer timerWithTimeInterval: 0.01
                                                 target: self
                                               selector: @selector(pumpEvent:)
                                               userInfo: nil
                                                repeats: YES];
    
        [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    }
    
    
    - (void) pumpEvent: (NSTimer *) timer
    {
        NSEvent *event = [NSEvent otherEventWithType: NSEventTypeApplicationDefined
                                            location: NSZeroPoint
                                       modifierFlags: 0
                                           timestamp: 0
                                        windowNumber: 0
                                             context: nil
                                             subtype: 0
                                               data1: 0
                                               data2: 0];
    
        if (event) {
            [NSApp postEvent: event atStart: YES];
        }
    }
    
    
    - (void) observeValueForKeyPath: (NSString *) keyPath
                            ofObject: (id) object
                              change: (NSDictionary *) change
                             context: (void *) context
    {
        NSLog(@"running changed to %@", [change objectForKey: NSKeyValueChangeNewKey]);
    }
    
    
    - (void) nestRun
    {
        _nestLevel++;
    
        NSLog(@"Entering nested run at _nestLevel = %ld", (long) _nestLevel);
    
        if (_nestLevel < 3) {
            [self performSelector:@selector(nestRun) withObject:nil afterDelay:0.05];
        } else {
            [self performSelector:@selector(callStop) withObject:nil afterDelay:0.05];
        }
    
        [NSApp run];
    
        NSLog(@"Returned from nested run at _nestLevel = %ld, isRunning = %ld", (long)_nestLevel, (long)[NSApp isRunning]);
    
        _nestLevel--;
    }
    
    
    - (void) callStop
    {
        NSLog(@"Calling stop:, _nestLevel = %ld", (long)_nestLevel);
        [NSApp stop:nil];
    }
    
    @end
    
    
    int main(int argc, const char * argv[])
    {
        NSApplication *application = [NSApplication sharedApplication];
        AppDelegate *delegate = [[AppDelegate alloc] init];
    
        [application setDelegate:delegate];
        [application run];
    
        return 0;
    }
  7. iccir commented on Jul 7, 2026

    @iccir
    Contributor

    I've been researching the history of #28981 for the past few days. First, no blame to anyone - NSRunLoop and friends are notoriously difficult. I had to review a bunch of code that I wrote 15 years ago :)


    Historically, wait_for_stdin was using -[NSApplication nextEventMatchingMask:…] with a date of -[NSDate distantPast] inside of a while loop. That was always going to result in heavy CPU utilization as the run loop wasn't being allowed to sleep and then wake itself up.

    This was changed to use -[NSApplication run]. That correctly loops with -[NSDate distantFuture] but has the side-effect of introducing this bug since re-entrant invocations to -run aren't supported.

    I do not understand the following comments at the top of wait_for_stdin():

    // Short circuit if no windows are active
    // Rely on Python's input handling to manage CPU usage

    There should be almost no CPU utilization when waiting for an event with an untilDate: of -[NSDate distantFuture]. The only thing that I can think of is that there was thrashing from constantly setting up and tearing down the NSFileHandle notification. There definitely was CPU usage prior to #28981, but that was due to the loop with -distantPast.

    Note that I'm uneasy about any type of "short circuit" once NSApplication has been instantiated - there's more parts of AppKit that need a running event loop even when windows aren't present. My best guess is that the constant churning of the run loop (prior to #28981) made everyone associate run loops with high CPU usage; hence, this check got added during development as a precaution.

    I'm still working on a solution that fixes both this while not regressing #26869 and #27515.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions