Interrupting Python Code
View SourceThis guide covers stopping Python code that is already running: a call that overruns its timeout, or work you want to cancel early. Without this, an Erlang-side timeout only stops waiting: the Python thread keeps running and the context stays busy. You need it whenever you run code you do not fully control, such as user-supplied scripts.
Timeouts interrupt automatically
Pass a timeout to any py_context call. When it expires the Python code is
interrupted and the context is free again:
{ok, Ctx} = py_context:new(#{mode => owngil}),
{error, timeout} = py_context:eval(Ctx, <<"while True: pass">>, #{}, 500),
%% The context is immediately reusable
{ok, 4} = py_context:eval(Ctx, <<"2+2">>, #{}, 5000).The caller still gets {error, timeout}. The Python side sees a
KeyboardInterrupt at the point it was executing.
Cancelling explicitly
To stop work before its timeout, or work started with infinity, call
py:interrupt/1 from any process:
Ctx = py:context(),
spawn(fun() -> py_context:eval(Ctx, <<"while True: pass">>, #{}, infinity) end),
%% ... later, from anywhere ...
ok = py:interrupt(Ctx).The interrupted call returns {error, interrupted}. py:interrupt/1 returns
not_running if the context is idle.
Catching it in Python
The interrupt arrives as KeyboardInterrupt, which derives from
BaseException, so ordinary handlers do not swallow it:
try:
do_work()
except Exception: # does NOT catch the interrupt
log_failure()Catch it explicitly to clean up, then re-raise:
try:
do_work()
except KeyboardInterrupt:
release_resources()
raiseCode that catches BaseException and continues will keep running. Destroy the
context to deal with that:
ok = py_context:destroy(Ctx).Limits
- CPython delivers an async exception at the next bytecode boundary. Code
blocked inside a C call (
time.sleep, a numpy kernel, a socket read) is not interrupted until that call returns. The call still times out on the Erlang side; the context becomes usable once the C call finishes. - An interrupt targets the context, not an individual request. Interrupting a context that just finished one call and started another stops the new one.
py:call/3,4andpy:eval/1,2useinfinityby default. Pass an explicit timeout, or usepy:interrupt/1, if you need a bound.