sometimes modules indeed distributed just using .pyd/.so and it’s nice that it’s possible to recognize the architecture they’re build for just from name.
It sounds nice but, unfortunately, you can’t really depend on the name in the general case. For one thing, the extension module (.so) file name is generated by whatever build process was used to build the extension and there are many different ones in use today in the Python world. Each one of them would need to be modified to incorporate some agreed-upon rules about what metadata is included in the file name and even then it still wouldn’t guarantee that the data is accurate or, even if accurate, useful as you still would need to know what architecture the Python interpreter doing the import will be running under. As we saw, on macOS, a single Python executable might be able to run as more than one on the same machine. Or there may be more than one Python executable installed on a machine that might try to import the same copy of an extension module.
Note that the execution CPU architecture is determined by the OS when a process is launched, usually automatically by examining the requested executable but, as we saw, it might be influenced by the use of the arch utility or by lower-level APIs. Once a process is launched, all code executed in it must be of a compatible architecture: you can’t mix and match, say, arm64 code and x64_64 code in the same process. Again, the dynamic loader normally enforces that transparently when dealing with multi-architecture binaries.
Also, while recent versions of macOS and of python.org macOS installers support two CPU architectures in fat binaries (what we’ve called universal2), there are at least three additional CPU architectures that are supported by older versions of macOS and older Mac hardware that are still in limited use in the world (i386, ppc, ppc64) and it is still possible to build Python and extension modules to support various combinations of all these architectures. And it is not at all trivial to determine ahead of time which combination of Python interpreter archs and extension module archs will work in a particular macOS version / Mac hardware environment. The rules could even change: during the transition period from PPC Macs to Intel Macs, a few releases of MacOS provided Rosetta which allowed PPC executables to run on Intel Macs but, with macOS 10.7, Apple removed support for Rosetta and suddenly some Python executables and/or extension modules no longer worked. No doubt, Apple will eventually drop support for Rosetta2 and running Intel-64 binaries on Apple Silicon Macs, possibly as soon as the next macOS feature release later this year.
The point of all this is that there is no foolproof method to guess ahead of time which extension modules are going to be able to execute in a particular Python environment on macOS. The best and surest way is to simply try importing the modules to see if they are loadable and, if necessary, catch the exception around the import statement.
I’ve seen some errors importing .so files compiled for arm64 having issues running on x86_64 and vice versa. Does it mean those users were using some special python that was prebuilt only for arm64 or x86_64 […]
It might. If you build Python from source on macOS, the default is build only for a single architecture, the “native” architecture of the machine. And most third-party distributors of Python for macOS, like Homebrew or MacPorts, typically only provide single-architecture pre-built binaries, although they may support building multi-architecture binaries from source.
But that brings out another potential pitfall: any external shared libraries that Python extension modules call also have to provide binaries with compatible architectures. For example, a standard installation of Python itself depends on external libraries like those for OpenSSL, Tk, etc and for a universal build of Python, universal versions of all those external libraries are needed (which the python.org macOS installers take care of). More importantly, extension modules included with third-party packages, however distributed (PyPI wheel, built from source, etc), can also call third-party shared libraries and which may or may not be included in the package. Even if the extension module binary itself is compatible with the running Python interpreter environment, an import could fail if the dynamic load of an extension module’s dependent shared library fails due to incompatible architectures.
And is it possible to have both x86_64 and arm64 modules imported during one session of universal python
As noted above, no. All the code executed in a macOS process has to be one architecture.
But isn’t it the same how it works on other platforms? Checking architecture is performed by the platform itself but suffixes just needed to detect the possible error early on and maybe use a different file.
It may be but, as I’ve tried to outline, macOS multi-architecture files and support for different architectures make it very difficult to determine by inspection what combinations are going to work. And, even it were easier, that would be putting the burden on the suppliers of packages containing extension modules to get the rules right somehow. Presumably there could be similar issues on other (non-macOS) platforms as well.
I’m not sure exactly what problem you are trying to solve here but, if you are trying to ensure that some installation is going to be able to load and start execution successfully, the best approach is to just test it. Perhaps a package provides some post-installation tests or you could write a simple Python program to find and try importing all .so files.
But this situation hasn’t really changed for a very long time: Python has supported multi-architecture (fat) builds on macOS for going on 20 years now. There are certainly ways things could be improved and we are open to suggestions. Perhaps with more information, we all could identify some specific use cases where people are running into problems and try to document or otherwise mitigate them.
Hope this helps!