ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 44

  1. Blog

random.choices vs sample vs choice: Localhost Lab

Aditya Challa·1 October 2026·4 min read

Summary
On this page
  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — pop 10 k / k 100 (p50)
  5. Scale sketch (picks/s)
  6. With vs without replacement
  7. Weighted path cost
  8. Reading it
  9. API cheat sheet
  10. Population size
  11. Determinism in tests
  12. Large k without replacement
  13. Pitfalls
  14. Reproduce
  15. Limits
  16. Takeaway

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

ArmReplacement
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

pop×k: 1k×10 · 1k×100 · 10k×100 · 10k×1k · 7 rounds · p50
metric: picks/s = k / p50_s

Script: lab-evidence/117-random-choices-vs-sample/results/run_lab.py.


Lead table — pop 10 k / k 100 (p50)

Armpicks/s
choices (with replacement)18900032
choice loop4951720
sample (without)4432033
choices equal weights489896

Scale sketch (picks/s)

Configchoicessamplechoice loop
1k×10707212419988013483109
1k×1001626280949346155930847
10k×1001890003244320334951720
10k×1k1222897545172635013084

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 Python choice loop here).
  • Unique subset → sample (must use without-replacement semantics).
  • Weighted draws → choices(..., weights=) (slower equal-weight path on this box).
  • Tokens / keys / passwords → secrets, not random (lab 88).

API cheat sheet

NeedCall
Bootstrap / bag drawschoices
Unique committee of ksample
One itemchoice
Secure tokensecrets (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 choices when uniqueness is required.
  • sample with k > len(pop) (ValueError).
  • Seeding random for security theater.
  • Building huge weight arrays every call.

Reproduce

python3 lab-evidence/117-random-choices-vs-sample/results/run_lab.py

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.

random.choicesrandom.samplerandom.choicewith replacementpython randomlocalhost labsrepicks/s

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.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

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

On this page

  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — pop 10 k / k 100 (p50)
  5. Scale sketch (picks/s)
  6. With vs without replacement
  7. Weighted path cost
  8. Reading it
  9. API cheat sheet
  10. Population size
  11. Determinism in tests
  12. Large k without replacement
  13. Pitfalls
  14. Reproduce
  15. Limits
  16. Takeaway
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove