PyCall not respecting class inheritance?

I have a Python package that I’m importing but class inheritance information is lost, so child class methods are not accessible. I can reproduce this problem with this simpler example here that uses PyCall but does not involve importing Python packages:

using PyCall

py"""
def defineDict():
    d = MyDict()
    d["a"] = 1
    d["b"] = 2
    return d

class MyDict(dict):
    def get_b(self):
        return self["b"]
"""

const defineDict = py"defineDict"
mydict = defineDict()

Then I get

julia> typeof(mydict)
Dict{Any, Any}

which is a Julia class converted from the Python parent class, so does not have the required method.

julia> mydict.get_b()
ERROR: type Dict has no field get_b

Even if I try to force the type to a PyObject, I still don’t inherit the methods.

julia> mydict2::PyObject = defineDict()
julia> typeof(mydict2)
PyObject
julia> mydict2.get_b()
ERROR: KeyError: key :get_b not found

I can confirm the method does not exist for this PyObject with PyCall.inspect[:getmembers](mydict2).

I can get it to work if I import the class method as a separate function.

const get_b = py"MyDict.get_b"
julia> get_b(mydict)
2

Is this the best way to access this method or it possible to force PyCall to respect class inheritance?

This is a conversion issue, not an inheritance one: you get the conversion to Julia Dict (and we lose the . member access).

The solution is to retrieve a real Python MyDict object (please note that I used py"..."o (that o at the end is doing the trick):

using PyCall

py"""
class MyDict(dict):
    def get_b(self):
        return self["b"]

def defineDict():
    d = MyDict()
    d["a"] = 1
    d["b"] = 2
    return d
"""

defineDict() = py"defineDict()"o
mydict = defineDict()
mydict.get_b() # 2

You attempt at mydict2::PyObject = defineDict() doesn’t work because when calling your instance of defineDict you already return a Julia Dict, and you only wrap it back to PyObject (so at that point the member information is already lost).

Another way to prevent the conversion to Julia Dict (and retain the Python object) is the following:

using PyCall

py"""
class MyDict(dict):
    def get_b(self):
        return self["b"]

def defineDict():
    d = MyDict()
    d["a"] = 1
    d["b"] = 2
    return d
"""
defineDict2() = pycall(py"defineDict", PyObject)
mydict2 = defineDict2()
mydict2.get_b() # 2

I have heard that PythonCall.jl
(which is as i understand it, more or less a more modern replacement for PyCall.jl)
does not have this issue.
Because it is much less aggressive than PyCall with converting things to julia types.

3 Likes

Interesting, thanks for the explanation. If the function and class definition were contained in a python package called module, can the code above be adapted? For instance

@pyimport import module
defineDict = pycall(module.defineDict, PyObject)

Does not work as it throws an error - not sure this is valid.

Oh, I thought PyCall succeeded PythonCall - I had it the other way around.

Yes, that should work. Here is an example using the math module:

using PyCall

function f()
    math = pyimport("math")
    pycall(math.sin, PyObject, 2.0)
end

f() # PyObject 0.90929742...

I see, you have to call through pycall. Thanks a ton.

It depends - pycall offers good control over the return type: but if the regular py"..." usage already returns the type you want (and in many cases it does), then pycall can be unnecessarily complex.

However, for your use case, pycall is the best approach to prevent the conversion to Julia’s Dict and retain the Python object (although you can also see that the same can be achieved by py"..."o approach).

1 Like