Massively simplify pyconvert?
Don't take a type, so it's just pyconvert(::Py). Symmetric with Py(x) for conversion in the other direction. Will be simpler so define rules and can just run up the MRO to apply them.
Could also have a override argument to possibly provide custom conversions (and same for Py).
Could additionally support pyconvert(T, x) as an alias for convert(T, pyconvert(x))::T. Or do something slightly snazzier which lets us skip over rules that cannot apply?
On reflection I think a good design is to have pyconvert(T::Type, x::Py). It works up the MRO and calls pyconvert_rule(T, Val(Symbol("module.type")), x).
The MRO can be computed once in a generated function, so we pay a dynamic dispatch cost only once per conversion. And we can hardcode some MROs and do explicit branching for common types like None, int, list, type.
Could also export e.g. pyconvert_int(T, x) as essentially pyconvert_rule(T, Val(Symbol("builtins.int")), x) (actually it runs up the whole MRO for the type) for when you know what you have.
Could also export special type checking functions like pyisnone etc.