[Steven D'Aprano
... + secrets.random calls the CSPRNG; it just returns a random number (integer?). There is no API for getting or setting the state, setting the seed, or returning values from non-uniform distributions;
The OpenBSD arc4random() has a very sparse API, but gets this part exactly right: uint32_t arc4random_uniform(uint32_t upper_bound); arc4random_uniform() will return a single 32-bit value, uniformly distributed but less than upper_bound. This is recommended over constructions like “arc4random() % upper_bound” as it avoids "modulo bias" when the upper bound is not a power of two. In the worst case, this function may consume multiple iterations to ensure uniformity; see the source code to understand the problem and solution. In Python, there's no point to the uint32_t restrictions, and the function is already implemented for arbitrary bigints via the current (but private) Random._randbelow() method, whose implementation could be simplified for this specific use. That in turn relies on the .getrandbits(number_of_bits) method, which SystemRandom overrides. So getrandbits() is the fundamental primitive. and SystemRandom already implements that based on .urandom() results. An OpenBSD-ish random_uniform(upper_bound) would be a "nice to have", but not essential.
+ secrets.choice similarly uses the CSPRNG.
Apart from error checking, that's just: def choice(seq): return seq[self.random_uniform(len(seq))] random.Random already does that (and SystemRandom inherits it), although spelled with _randbelow().