python 74 lines · 2 tabs

Debounce Repeated Function Calls With a Thread-Safe Python Decorator

Shared by codesnips Jul 2026
2 tabs
import functools
import threading


def debounce(wait):
    def decorator(func):
        lock = threading.Lock()
        state = {"timer": None}

        def call(args, kwargs):
            with lock:
                state["timer"] = None
            func(*args, **kwargs)

        @functools.wraps(func)
        def debounced(*args, **kwargs):
            with lock:
                if state["timer"] is not None:
                    state["timer"].cancel()
                timer = threading.Timer(wait, call, args=(args, kwargs))
                timer.daemon = True
                state["timer"] = timer
                timer.start()

        def cancel():
            with lock:
                if state["timer"] is not None:
                    state["timer"].cancel()
                    state["timer"] = None

        def flush(*args, **kwargs):
            cancel()
            func(*args, **kwargs)

        debounced.cancel = cancel
        debounced.flush = flush
        return debounced

    return decorator
2 files · python Explain with highlit

Debouncing collapses a burst of rapid calls into a single execution that runs only after the caller goes quiet for some interval. It is the standard tool for taming noisy triggers: a search box that fires on every keystroke, a file watcher that emits many events per save, or a resize handler that would otherwise flood a backend with requests. The idea is simple but the implementation has sharp edges around timing and thread safety, which is why a reusable decorator is worth building once and reusing everywhere.

The debounce decorator file implements the core pattern with threading.Timer. Each decorated function keeps a small piece of per-function state guarded by a threading.Lock. On every call, debounced cancels any pending Timer and schedules a fresh one for wait seconds later. Only when calls stop for the full wait window does the timer fire and actually invoke the wrapped function. This is trailing-edge debounce: the last set of arguments wins, which is exactly what a search-as-you-type feature needs. The lock matters because Timer callbacks run on separate threads, so cancelling and reassigning self._timer without synchronization would race.

Two helper methods are exposed on the wrapper via functools.wraps-preserved attributes: cancel() drops a pending call entirely, and flush() runs it immediately. These are important for shutdown paths and tests, where waiting on real wall-clock timers is undesirable. Note the pitfall the code sidesteps: return values are effectively discarded, since the real call happens later on a timer thread — debounce is for side effects, not for functions whose result the caller awaits inline.

The search box usage file shows the decorator in a realistic setting. SearchController.on_keypress is debounced at 0.3 seconds, so typing "laptop" issues one query instead of six. The flush and cancel hooks are wired to explicit submit and blur events. A developer reaches for this whenever an event source is faster than the work it triggers, trading a little latency for far fewer, more meaningful invocations.

Share this code

Here's the card — post it anywhere.

Debounce Repeated Function Calls With a Thread-Safe Python Decorator — share card
Link copied