The ctypes library in Python is basically a wrapper around the C library libffi which is a library that let's us invoke arbitrary functions in the process's address space given information at runtime instead of compile-time.
The same library can be used in Haskell together with the dynamic linker to achieve the same effect:
import Foreign.LibFFI
import System.Posix.DynamicLinker
example :: IO ()
example = do
dlopen "mylib.so" [RTLD_LAZY]
fn <- dlsym Default "some_function"
fn2 <- dlsym Default "some_function2"
a <- callFFI fn retCInt [argCInt 33]
b <- callFFI fn2 retCInt [argCInt 33]
return ()
This isn't ideal for a lot of reasons, and I wouldn't recommend building a large set of bindings like this for the same reason that many Python developers frown on using ctypes for bindings except as a last resort. For one it will have some overhead (roughly the same overhead that ctypes has) for each function call. As compared to a ccall unsafe FFI function from Haskell which has basically no overhead and is as cheap as a native C function call.
Second, instead of having arguments with nice simple Haskell types like Int and Float arguments to functions are now all in a giant libffi sum type Arg that's opaque to the type system and is quite easy to introduce subtle bugs.
If you can link the C library at compile-time and forward declare the functions, either using the regular Haskell FFI ( -XForeignFunctionInterface ) and/or tools like c2hs it will make life much easier.