I have not thought about sessions and asynchronous execution. It would
mean a queue and a different way to pass code to the process, which I
have not thought through yet. What to do when there are dependencies for
example,...
A good way of converting synchronous into asynchronous code is to use futures/promises. Usually, that's done via data structures, but within Emacs buffers, it could be done via text strings.
How might that work? org-babel-execute:python could wait for, say, 0.1 sec for an immediate result. If the computation doesn't finish within that time, it returns a "future", a magic string like "org_mode_future_result(1234) ###MAGIC###". This would then get inserted as output into the org-mode buffer. Later, when the actual result becomes available from the subprocess, that invokes a callback on the org-python mode buffer and replaces tihs magic string with the actual result, and dequeues and executes the next command if necessary.
(Picking a Python-parseable expression would even allow futures to be used in some other Python computations as if it were an actual value.)
I think that would allow much of the current API to remain in place. Obviously, some things simply can't work. For example, org-babel-reassemble-table expects an actual result, not a future; such post-processing would have to move to a hook function, which probably would be cleaner anyway.