Repository navigation
Make pdb asyncio aware #121468
Description
Feature or enhancement
Proposal:
Currently pdb does not know anything about asyncio, even though it's a feature officially supported for a long time. I will add supports for asyncio in roughly 3 steps (does not have to be exact 3 PRs):
pdbwill generate some passive info for asyncio. If the user hits a breakpoint inside an asyncio task, there will be a one line message with the current stack indicating which task the user is in. A convenience variable$_asynctaskwill be added to refer to the current task.pdbwill provide a way for the user to examine all the tasks in the current event loop with a new command. The user can inspect how many tasks are there and their status. Maybe they can even check the frames for each task.pdbwill enable users to switch between tasks, aka letting users to specify "run until I'm in this task".pdbwill also possibly provide a way for the users to cancel the task.
In theory, many of the features are already possible with manual code as pdb can execute arbitrary code anyway. However, I believe it's helpful for the users to have a more convenient way to debug their async programs.
This was briefly mentioned in the language summit at PyCon this year, no one objected (maybe because they have other stuff that they were more against :) ).
This should not impact any user experience with the existing code that not involves asyncio.
If you have any other suggestions or objections, please let me know.
Has this already been discussed elsewhere?
No response given
Links to previous discussion of this feature:
No response
Linked PRs
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsTodo
Activity
kumaraditya303 commentedon Sep 29, 2024
I have some questions:
gaogaotiantian commentedon Sep 29, 2024
Switching is basically wait until in that task.
pdbwill have a callback for each function call(including entering generator) and we just need to check if the callee is what we need.Multi-threading is not supported at all in
pdb. Anything involves multi-threading is a non-blocker because - well it won't work anyway.I did not think about async generators specifically, but as far as I understand, it's not that special is it? Maybe we can deal with it when there's a concrete example.
What I'm trying to do now is to have some level of support of
asyncio, because have none now (actually I don't think the debuggers out there have support it much either). There will be issues that are difficult to solve, and there might be some things that are not achievable in the short term. There are things that we can't do in a single process single thread sync Python program. We may not be able to build a perfect all-can-do debugger for async, but we should still try.kumaraditya303 commentedon Nov 1, 2024
I look forward to try a prototype of this!
gaogaotiantian commentedon Nov 1, 2024
@kumaraditya303 could you take a quick look at #124367? It's just a draft for now because I need to change all the asyncio related pdb doctests before merging it. It implemented step 1. One thing I'd really like to know is if the information
Task-1: <main pending>enough. asyncio tasks have their own repr and of course we can use that in pdb directly, but the repr is really long, including a lot of information. The short version is something we want to show the users every time they stop inside an async task and I think it should be more concise. They can always do a$_asynctaskto get the current running task. Do you think there are more critical information that the user will very often want to know from their task? I'm not the most heavy asyncio user in the world and that's the area where I need suggestions from experts.kumaraditya303 commentedon Nov 1, 2024
Task-1: <main pending>isn't so useful,runningwould be better thanpendingand it should print the callbacks of the task too.As for convenience variable, maybe we can have one for the current contextvars used by the task?
gaogaotiantian commentedon Nov 1, 2024
I can remember the enter task and set that task to
running, that's doable. All the other tasks would bepending,cancelling,cancelledorfinishedI think.For the callbacks, one of the issue I had was there could be many and it could be long. Do you think something like
Task-1: <main pending> cb=[_run_until_complete_cb()]would be helpful? Again, this is the information we display immediately (and always) when the user hits the breakpoint or steps, only the most important information should be displayed here. In normal cases, it would beThe user can always get the detailed information with
$_asynctask:But this line is too long to show in the debugger every time the user stops (it could be even longer).
As for the convenience variable, we have a few ways to show that. The simplest is just
Then you can do
which is always correct but a bit boring.
Say we have a
foo = contextvars.ContextVar('bar', default='default'), what do you want in a debugger? You can have a dedicated command likeOr you can have a proxy to support
Getting the current context is trivial, the question is what the user need to interface with the context.
gh-121468: Add current asyncio task as a convenience variable in pdb (#…
gh-121468: Fix the doctest failure for asyncio test on pdb (#131258)
pythongh-121468: Add current asyncio task as a convenience variable i…
pythongh-121468: Fix the doctest failure for asyncio test on pdb (pyt…
gh-121468: Ensure PDB cleans up event loop policies after using async…
pythongh-121468: Ensure PDB cleans up event loop policies after using…
gh-121468: Support async breakpoint in pdb (#132576)
ktbarrett commentedon May 30, 2025
Is there any possibility this could be made to be not
asynciospecific? There are other concurrency frameworks liketrioand thecocotb(which I maintain) that could benefit from this. This could be as simple as providing pdb functionality to switch which coroutine's stack is the stack being operated on by the debugger. We could handle the rest.ktbarrett commentedon Aug 11, 2025
Another issue is that this makes
breakpointproblematic. Ideally you could use theset_trace_asyncwhen in an asyncio Task (or generally just an async function), butset_traceotherwise.johnslavik commentedon Sep 7, 2025
I'm finding one thing hard about
set_trace_async. I think it touches a more general problem.Consider this example:
The problem is that you can only await before you step further:
The tests don't seem to be exercising that path.
The reason this happens is that we rely on
set_trace_async()shim frame to await the ephemeral coroutine from a top-level await instruction, but there is no shim frame as soon as we leaveset_trace_async()because ofn/s.As far as I understood, something as straightforward as running an async prompt in a task like in the asyncio REPL is not an option here:
cpython/Lib/asyncio/__main__.py
Line 62 in c117b03
That said, this problem is about generators as well; the problem of
awaitis essentially the same as withyield from.I was wondering if there could be a way to execute the ephemeral coroutine constructed from a top-level await instruction without having to rely on the shim frame. This would make
awaits work everywhere it's needed without even having to worry about awaitingset_trace_async.My guess was that it could be feasible if generators (and coroutines, and async gens) were mutable. That is, if there was a way to make a generator temporarily yield from a new object (a new generator that can be written in a REPL, let's call it
gi_debugging_extra_items) and then, after such temporary iterator is exhausted, usegi_yieldfromas usual. (gi_yieldfromcannot be written to because it's a getter that finds the delegation target in the frame stack).That would roughly work like:
->
->
->
I'm not sure whether that's a good path to go down. Perhaps there could be an easier way to mutate generator-like objects for debugging purposes. How can we lie to
_PyGen_yf?In any case, if that can be done, the problem of await/yield from seems to be fully solved.
johnslavik commentedon Sep 7, 2025
It's essentially about replacing
frame.f_generator.gi_yieldfromwith anitertools.chain(user_yieldfrom, frame.f_generator.gi_yieldfrom)once (but note thatgi_yieldfromis just for inspection and the interpreter actually uses_PyGen_yfwhich reads the delegation target from the stack. So we have to overwrite what_PyGen_yffinds.f_generator.gi_yieldfromuses a getter that just wraps_PyGen_yf).Maybe just overwrite the stack pointer? We wouldn't need an extra field in every generator-like construct.
Or perhaps monkeypatch the entire
f_generator?Or add a field to generator, say
gi_debugging_extra_itemsand try to yield from it instead of the actual generator insend_ex2. I'll work on that in mymutable-generatorsbranch.EDIT: This is likely impossible without patching the bytecode to interleave it with dynamic new yields, but I haven't confirmed this.
kumaraditya303 commentedon Aug 3, 2026
@gaogaotiantian Should this be closed or is there more to do?
gaogaotiantian commentedon Aug 12, 2026
Hi @kumaraditya303 , the first task is finished but I still want to work on async task list and switch to other async tasks. I will also take a look at the feedbacks of this feature and try to improve it. This will be on the list for my core dev sprint :)
johnslavik commentedon Aug 12, 2026
@gaogaotiantian, let's chat about it there!