Do I understand PythonKit correctly?

I am running into a few issues using PythonKit For example trying to access a nonexistent property of PythonObject it throws fatalError

guard let member = checking[dynamicMember: memberName] else {
                fatalError("Could not access PythonObject member '\(memberName)'")
            }

Why ? It kills the application.
Or even worse: The library assumes that python code that is used never throws an exception so it does:

func dynamicallyCall(
        withArguments args: [PythonConvertible] = []) -> PythonObject {
        return try! throwing.dynamicallyCall(withArguments: args)
    }

Again calling try! should only be done when you are absolutely sure that the call never throws an exception as if you are wrong the application crashes. It exactly what happens.
Naively I would expect that PythonKit would introduce its own exception and these methods would throw it instead on stopping or crashing the client code. But I assume I am doing something wrong here.

The issue is that there's s mismatch of design philosophy (dynamically typed vs statically typed+compiled) and error handling (exception raising vs error throwing) that is pretty hard to reconcile in a bridge layer like this.

Swift's error-throwing mechanism is explicit and requires participation of the caller. Unlike a thrown exception in say Java or C#, which has an automatic stack unwinding process, Swift's error throwing model is essentially just sugar around returning a Result<Success, Failure> and branching (done by the caller) on the returned result.

If dynamicallyCall wanted to be throws, then every single call to a python method would need a try, and the caller of that would need a try, and so on. Most likely, people will just try!, and end up in pretty much the same place.

If people call try ! on an a throwable method they probably assume that their code does not throw. As the things stand now PythonKit usability is limited to command line utilities that usually stop on error.
Including it in a UI or Server application that is supposed to run forever is not an option. I solved my problem by wapping every Python call I make in a Python wrapper that catches exceptions and puts them into a dictionary like:

def submit(config):
    result = {
      "task": None,
      "error": None,
    }

    try:
        result["task"] = original_susbmit_that_can_throw(config)
    except Exception as err:
        result["error"] = str(err)
    return result

That is ugly and time consuming. I would much prefer a throwable Swift. ... call.

You can make your own equivalent of PythonObject whose dynamicallyCall is marked throws.

I fear you'll find that the ergonomics of that are also pretty rough

I think you’re looking for PythonObject.throwing to get the non fatal error version. I agree with you the ‘default’ error handling is not that useful.

1 Like

Ahhhh I didn't know about this! ThrowingPythonObject is exactly what I was suggesting to hand-roll, but it's great to see it's built in.

I'll chime in with a small frame challenge:

Suppose you tried to call a non-existent method on an object, and an error was thrown, and you could catch it. What would you do with it?

What possible recovery sequence could you have that can compensate for a typo, a misread of the API documentation, a confusion of one type of object for another, etc.?

Regardless of whether you're using Python normally or through PythonKit, I'd argue that there really isn't much you can meaningfully do at runtime to recover from such an error, so there's practically no reason to try. Your program is broken, and you just need to fix it.

And for all you know, whatever typo you made in your calling code, you could have just as easily made in your typo-recovery code. Who recovers the typo-recovery code's typos?

1 Like

It is not non existing method only. Any error crashes the application. Let's say you are writing a calculator in Swift UI that delegates calculation to Python. The user enters 5/0. You cannot even show the error message the application crashes.
If you have an http server a small error somewhere in an unimportant obscure place can bring down the whole site.
A library developer should never ever kill the application from the library code. It should always either throw an exception or return some error condition and let the client deal with it.

1 Like

Just use PythonObject.throwing, which does what you want.

1 Like

Fair enough, I was just addressing the concrete example that you mentioned

Agreed. I wonder how Vapor does this. Requests should be independent.

fatalErrors are indeed a big issue for server side swift, because Swift enforces such even for relatively minor things like out of range exceptions. (i.e. things which would usually allow a clean shutdown)

1 Like

I think killing the application by the language is definitely not the brightest idea.Tthink about it, you cannot even log the condition that causes the error. I would vote with both hands to deprecate any hard stops from the language.

I agree in principle, but I don't know of any way to practically implement it.

The only two options I'm aware of are:

  1. Effectively make every function throws, which requires the caller to always handle the possibility of a thrown error.
  2. Add exception handling, and take on the binary bloat cost for every function, whether an error can be thrown in it or not.

Did someone call me? Graphing Calculator embeds the SymPy computer algebra system, calling Python from Swift to implement the Simplify and Solve menu items to handle integration and differential equations. Catching and recovering gracefully from Python errors is necessary, and works straightforwardly.

4 Likes