A permutation test needs no distributional assumptions: if the treatment did nothing, the group labels are arbitrary, so shuffling them shows how big a difference chance alone produces.
Implement:
permutation_test(a, b, stat, n_perm, rng): the observed statistic is stat(a, b). For each permutation, shuffle the pooled data with rng.permutation, split it back into groups of the original sizes, and compute the statistic. Return the two-sided p-value (count + 1) / (n_perm + 1), where count is the number of permuted statistics with abs(perm) >= abs(observed).diff_means(a, b) and diff_medians(a, b): statistics of the form b - apaired_permutation_test(before, after, n_perm, rng): for paired data, randomly flip the sign of each difference (rng.choice([-1, 1], size=n)) instead of shuffling, using the mean difference as the statistic, with the same p-value formula