However, it seems that numpy (written in C), one of the most popular Python libraries for numerics, has a function numpy.empty. The example shows printing the un-initialized values (i.e. reading uninitialized).
How come this is sound, but reading un-initialized in Rust is not? Is it because the code has already been compiled and thus no optimizations can be performed?
That calls PyArray_NewFromDescr, which calls npy_alloc_cachehere, which calls _npy_alloc_cachehere, which calls allochere. I can't find where alloc is defined to check whether it is a malloc.
I think the only reason it doesn't result in miscompilations is because numpy is dynamically linked to cpython and as such it is not possible to the compiler to observe that uninitialized memory was used.
The sad truth is that most "C programmers", or people who write C code, don't really know C. The same goes for C++. Universities are full of courses that teach simplistic, semi-false, or downright wrong gut instincts while teaching students C and/or C++. Some popular internet forums are probably even worse. People who "learned" pre-standard C before 1989 or something now write UB-ridden code in horrible style for all sorts of systems, and they pass on the "knowledge" to the younger generations.
The reading and printing of uninitialized memory in NumPy's C core is not correct. It is Undefined Behavior, but I think fixing it is a ship that has long sailed.