Plate 44
random.choices vs sample vs choice: Localhost Lab
Aditya Challa4 min read
Intro — what this post promises
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:
- secrets vs urandom localhost lab
- statistics quantiles vs manual localhost lab
- graphlib topo vs manual localhost lab
- functools cache vs lru localhost lab
- tomllib vs json localhost lab
- path glob vs fnmatch localhost lab
- dataclass replace vs manual localhost lab
- islice vs list slice localhost lab
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.
Arms
| 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) |
Lab topology
Script: lab-evidence/117-random-choices-vs-sample/results/run_lab.py.
Lead table — pop 10 k / k 100 (p50)
| Arm | picks/s |
|---|---|
| choices (with replacement) | 18900032 |
| choice loop | 4951720 |
| sample (without) | 4432033 |
| choices equal weights | 489896 |
Scale sketch (picks/s)
| 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 |
With vs without replacement
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.
Weighted path cost
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.
Reading it
- Duplicates OK →
choices(faster than a Pythonchoiceloop here). - Unique subset →
sample(must use without-replacement semantics). - Weighted draws →
choices(..., weights=)(slower equal-weight path on this box). - Tokens / keys / passwords →
secrets, notrandom(lab 88).
API cheat sheet
| Need | Call |
|---|---|
| Bootstrap / bag draws | choices |
| Unique committee of k | sample |
| One item | choice |
| Secure token | secrets (lab 88) |
Population size
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.
Determinism in tests
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.
Large k without replacement
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.
Pitfalls
- Using
choiceswhen uniqueness is required. samplewithk > len(pop)(ValueError).- Seeding
randomfor security theater. - Building huge weight arrays every call.
Reproduce
Evidence: summary.json, summary.txt.
Limits
One Linux box. Integer populations. Not NumPy RNG. Not crypto.
Takeaway
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
What I found running this
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.
Related links
Plate 17
platform vs os.uname Inventory: Localhost Lab
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.
1 Oct 2026
Plate 75
uuid.uuid4 vs uuid.uuid1: Localhost Lab
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.
1 Oct 2026
Plate 50
signal vs threading.Event Wakeup: Localhost Lab
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.
1 Oct 2026