Is a pseudo-random number generator suitable for assigning treatments in an experiment? I am working on a web service for randomly allocating users to treatment groups.
My belief is that using a pseudo-random number generator as the basis of those allocations is reasonable, but I am not sure that it's reasonable in all cases. Previously I have seen colleagues avoid using pseudo-random generators for the purpose of allocating users to treatment groups, although we didn't discuss it much.
I want to be sure that any hypothesis testing which uses a pseudo-random number generator will give valid results.
Currently my implementation is using Python's random module, although I am considering using a linear congruential generator so that the same implementation can be built in any language, and especially so that it can be implemented in a Snowflake javascript UDF.
def get_cohort(identifier: Any, experiment: Any, cohorts: List[str] = DEFAULT_COHORTS):
    """
    Randomly assign a value from the `cohorts` list to the given experiment and
    identifier.

    Calling this function with the same identifier, experiment, and list of
    cohorts will return the same selection from the cohort list every time.
    """

    seed = get_seed(identifier, experiment)  # a unique integer
    formatted_cohorts = _format_cohorts(cohorts)
    random = Random()
    random.seed(seed)
    index = int(random.random() * len(formatted_cohorts))
    return formatted_cohorts[index]

 A: A PRNG doesn't buy you any more randomness than the hash, and requires you to be able to port the PRNG to any other language you want to use. Instead, just use the hash, as your hashing function is probably already implemented in the language you want. I see you are using md5, so you can just squash it to [0, 1] by dividing by the maximum md5 hash value of $2^{128}$:
def get_cohort(identifier: Any, experiment: Any, cohorts: List[str] = DEFAULT_COHORTS):
    """
    Randomly assign a value from the `cohorts` list to the given experiment and
    identifier.

    Calling this function with the same identifier, experiment, and list of
    cohorts will return the same selection from the cohort list every time.
    """

    seed = get_seed(identifier, experiment)  # a unique integer
    squashed = seed / (2**128)

    formatted_cohorts = _format_cohorts(cohorts)
    index = int(squashed * len(formatted_cohorts))
    return formatted_cohorts[index]

We can see that the resulting "probabilities" are uniformly distributed:
import hashlib
import random
import string

import seaborn as sns

def _get_bytes(value) -> bytes:
    if type(value) == bytes:
        return value
    elif type(value) == str:
        return value.encode('utf8')
    elif type(value) == float:
        if value == int(value):
            return str(int(value)).encode('utf8')
        else:
            return str(value).encode('utf8')
    else:
        return str(value).encode('utf8')

def get_seed(identifier, experiment) -> int:
    """
    Convert any combination of identifier and experiment to a random integer.
    Calling this function with the same identifier and experiment value
    will return the same integer every time.
    """

    identifier = _get_bytes(identifier)
    experiment = _get_bytes(experiment)
    hexdigest = hashlib.md5(identifier + experiment).hexdigest()
    seed = int(hexdigest, 16)
    return seed

def random_string():
    letters = string.ascii_letters
    return ''.join(random.choices(letters, k=10))

seeds = [get_seed(random_string(), "experiment_1") / (2**128) for _ in range(1000000)]

sns.distplot(seeds)


