np.random.seed(0) seeds a single global MT19937 instance shared by every module in the process. Any library that also calls into np.random consumes from the same stream, so results become reproducible only if the entire call order stays fixed. Adding a log line that draws a random sample changes every downstream number.
Since NumPy 1.17 the recommended interface is an explicit generator: rng = np.random.default_rng(12345), then rng.normal(size=10). The generator is an object you pass around, so two components cannot interfere, and it uses PCG64 rather than the Mersenne Twister.
The legacy functions are not deprecated for removal and their stream is guaranteed stable, which is exactly why they must not be improved; the new Generator makes no such guarantee across versions, so pin NumPy if you need bit-identical output across upgrades. For parallel work, rng.spawn(n) produces independent child generators, which is the correct way to seed workers rather than adding the worker index to a base seed.