4 min read
Pick k items from a population via random.choices (with replacement), random.sample (without), and a loop of random.choice. This lab reports picks/s on Linux localhost.
Frame: with vs without replacement. It is not secrets/CSPRNG (lab 88) — random only, non-crypto.
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. random.Random(123) per process.
Verdict up front (pop=10 000, k=100): choices ~18900032 picks/s; choice loop ~4951720; sample ~4432033.
| Arm | Replacement |
|---|---|
random.choices(pop, k=k) | with |
[choice(pop) for _ in range(k)] | with |
random.sample(pop, k) | without |
choices(..., weights=…) | with (weighted path) |
Script: lab-evidence/117-random-choices-vs-sample/results/run_lab.py.
| Arm | picks/s |
|---|---|
| choices (with replacement) | 18900032 |
| choice loop | 4951720 |
| sample (without) | 4432033 |
| choices equal weights | 489896 |
| Config | choices | sample | choice loop |
|---|---|---|---|
| 1k×10 | 7072124 | 1998801 | 3483109 |
| 1k×100 | 16262809 | 4934615 | 5930847 |
| 10k×100 | 18900032 | 4432033 | 4951720 |
| 10k×1k | 12228975 | 4517263 | 5013084 |
choices may repeat indices; sample guarantees distinct elements. Product bugs often come from “shuffle-like” UX that silently allows duplicates. Write the invariant in the function name or type (UniqueDraw vs BootstrapDraw) before optimizing picks/s.
Equal weights still took the weighted implementation here (~489896 picks/s at 10k×100) — much slower than unweighted choices. If weights are uniform, omit them.
choices (faster than a Python choice loop here).sample (must use without-replacement semantics).choices(..., weights=) (slower equal-weight path on this box).secrets, not random (lab 88).| Need | Call |
|---|---|
| Bootstrap / bag draws | choices |
| Unique committee of k | sample |
| One item | choice |
| Secure token | secrets (lab 88) |
sample cost grows with uniqueness bookkeeping; choices stays a tight with-replacement loop. At 10k×1k on this box, choices still led (~12228975 picks/s vs sample ~4517263). Re-measure if k approaches len(pop) — algorithms inside sample change character near that edge.
Seed with random.Random(seed) (as this lab did) or random.seed for reproducible unit tests. Do not confuse reproducible draws with cryptographic unpredictability — CI flakes from unseeded random are a test smell, not a reason to pull in secrets for shuffle fixtures.
When you need almost the whole population in random order, sample(pop, k=len(pop)) is a shuffle; for a full permutation random.shuffle on a copy may be clearer. Mid-sized k (as here) is where sample vs choices semantics dominate the API choice more than raw picks/s.
choices when uniqueness is required.sample with k > len(pop) (ValueError).random for security theater.Evidence: summary.json, summary.txt.
One Linux box. Integer populations. Not NumPy RNG. Not crypto.
At pop=10 k / k=100, choices ~18900032 picks/s beat sample ~4432033 and a choice loop ~4951720. Match the API to replacement rules — never to secrets-grade needs.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5. pop10k/k100: choices 18900032 picks/s; sample 4432033; choice loop 4951720. Not lab 88 secrets. Affiliates: 0. Evidence: lab-evidence/117-random-choices-vs-sample/. Run.
Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read
Hands-on uuid.uuid4 vs uuid.uuid1 ID generation lab: real ops/s plus version/node checks, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20263 min read
Hands-on signal SIGUSR1 vs threading.Event wakeup lab: real p50 latency in microseconds, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read