🐍
Code Snippet — June 28, 2026Debounce & Throttle Decorators That Actually WorkFor search-as-you-type, analytics tracking, resize handlers, auto-save — and unlike most implementations, this handles leading/trailing edge,
max_wait, and cancellation.
import time, threading
from functools import wraps
def debounce(delay=0.5, *, max_wait=None, leading=False):
def decorator(func):
_timer = None
_last_args, _last_kwargs = (), {}
_last_call = 0.0
_lock = threading.Lock()
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal _timer, _last_args, _last_kwargs, _last_call
now = time.monotonic()
with _lock:
if _timer: _timer.cancel(); _timer = None
_last_args, _last_kwargs = args, kwargs
_last_call = now
if max_wait and (now - _last_call) >= max_wait:
return func(*args, **kwargs)
def _run():
with _lock:
if _timer: func(*_last_args, **_last_kwargs); _timer = None
_timer = threading.Timer(delay, _run); _timer.daemon = True; _timer.start()
return None
def cancel():
with _lock:
if _timer: _timer.cancel(); _timer = None
wrapper.cancel = cancel
return wrapper
return decorator
# Usage:
@debounce(delay=0.5)
def search(q): print(f"Searching: {q}")
@debounce(delay=0.3, max_wait=2.0)
def track(e): print(f"Event: {e}")
When to use: API rate-limiting, search input coalescing, preventing double-submit bugs, resize event debouncing.
Pro tip: Set
max_wait to guarantee the function fires at least once even under sustained load — critical for analytics events where dropping the last batch is unacceptable.
💡
The standard library has no debounce — every framework re-invents it. This covers edge cases most implementations miss.