LESSON 139 · DEEPER PYTHON30 min

Use Weighted Random Sampling in Python

Draw one outcome according to validated nonnegative weights using an injected random generator.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Use Weighted Random Sampling in Python

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    Imagine a simulation with two outcomes. A has weight zero and B has weight one. Can A appear? If the program is using the weights correctly, the answer is no. This gives us a sharper test than asking whether a short sample merely looks plausible. A different example with weights one and three would give both outcomes a chance, but even a thousand draws would not be guaranteed to follow an exact one-to-three count. Today we will build a wrapper that accepts a population, a weight vector, and a random generator. We will first run a plausible-looking broken version, then inspect the API call that actually determines the distribution.

  2. 00:41

    Uniform choice gives each item an equal chance. A game, simulation, or classroom experiment may instead assign different relative weights. Python's random.choices accepts a population and weights; a zero weight makes that outcome impossible when another weight is positive. We inject a random.Random instance so tests can reproduce a sequence without modifying global random state. Weights are relative values, not percentages: one and three give a one-to- three ratio, and ten and thirty describe the same distribution. This tool is for simulation, not for passwords or security tokens.

  3. 01:21

    The broken run holds a weights list next to the items but calls choice rather than choices. Watch the terminal sequence: with the selected seed, a zero-weight A may appear, which contradicts the stated rule. The unused variable can fool a quick code review because its presence makes the function look as though it considered weights. A seed does not fix that. Seeding means we can replay the same incorrect result and discuss it precisely. The question is which argument reaches the random method. Pause on the line with choice and observe that it receives only items. That method performs a uniform draw; it cannot infer our separate weight list.

  4. 02:03

    The broken run calls rng.choice even though a weight list is present. The code looks random and may pass a casual test, but it ignores the policy altogether. With an outcome weighted zero, any selection of that outcome reveals the bug decisively. Predict whether the uniform sampler can still pick A when A has weight zero in a separate list. A seeded generator makes the diagnostic repeatable, but a seed does not make an incorrect algorithm weighted. It only makes the same incorrect sequence easier to inspect. In the diagnosis we make two draws for several seeds: one through choice and one through choices with weights.

  5. 02:44

    The contrast does not depend on a vague impression of frequency. Weighted draws never produce A because its weight is zero and B's is positive. Uniform draws can produce A. Notice that choices returns a list because it supports requesting k outcomes at once. For our single-outcome helper, we pass k equal to one and select the first element of that list. We also need to decide what happens for empty items, mismatched lengths, negative numbers, NaN, infinity, and an all-zero total. Those inputs should fail at our boundary before we ask the library to sample. Before calling choices, validate that the population is nonempty, that the two lists

  6. 03:26

    have equal length, that every weight is finite and nonnegative, and that their sum is positive. Negative or NaN weights do not describe a useful distribution. Then call rng.choices(population, weights=weights, k=1)[0]. It returns a one-element list because choices is designed to draw any number of outcomes. The index selects the single requested result. This small wrapper gives a readable API and a place to enforce the input contract. The fixed wrapper checks the shape and each weight before drawing. The code uses math.isfinite so NaN and infinity cannot enter an undefined or

  7. 04:07

    misleading probability calculation. All weights must be nonnegative, and at least one must be positive. We pass an injected Random instance instead of calling a global function, which keeps tests independent and makes a simulation reproducible when we deliberately reuse a seed. Then the call to choices uses the weight vector. The final index extracts one item, matching our function name and task contract. On screen, five different seeds still all choose B when A's weight is zero. Trace the validation order before the final library call. The nonempty and equal-length checks prevent ambiguous pairing of item and

  8. 04:47

    weight. The finite, nonnegative, positive-total checks make zero meaningful without allowing all outcomes to be impossible. The method receives both lists and k equal to one; its return type is a list, so index zero produces a single selected item. Nothing here asserts a particular answer for a positive-weight random draw. Our exact unit tests use the zero-weight invariant and the input errors instead. This distinction between deterministic invariants and probabilistic estimates is the main testing lesson behind the implementation.

  9. 05:27

    The last run checks twenty seeds and observes the set of possible results. That set contains only B. It also passes an all-zero vector and confirms that we reject it with ValueError. These are boundary tests, not proof that a one-to-three example will always have exactly one A for every three B results. If you want to study a distribution, run many trials and state sampling uncertainty. If you need a password, token, or security choice, do not reuse this simulation helper; use a cryptographic source designed for that purpose. In the article exercise, try changing the relative weights while preserving their ratio and explain what stays the same.

  10. 06:10

    The final run checks a zero-weight boundary across multiple draws and rejects an all-zero weight list. It does not claim that five draws prove a three-to-one ratio; randomness needs many observations for a statistical estimate and even then individual outcomes vary. A zero-weight outcome is a deterministic invariant, which makes it a stronger unit test. If the application needs cryptographic unpredictability, use the secrets module and design that requirement separately. For ordinary reproducible simulations, pass a Random object instead of relying on hidden global state.

Understand the concept

Uniform choice gives each item an equal chance. A game, simulation, or classroom experiment may instead assign different relative weights. Python's random.choices accepts a population and weights; a zero weight makes that outcome impossible when another weight is positive. We inject a random.Random instance so tests can reproduce a sequence without modifying global random state. Weights are relative values, not percentages: one and three give a one-to-three ratio, and ten and thirty describe the same distribution. This tool is for simulation, not for passwords or security tokens.

The broken run calls rng.choice even though a weight list is present. The code looks random and may pass a casual test, but it ignores the policy altogether. With an outcome weighted zero, any selection of that outcome reveals the bug decisively. Predict whether the uniform sampler can still pick A when A has weight zero in a separate list. A seeded generator makes the diagnostic repeatable, but a seed does not make an incorrect algorithm weighted. It only makes the same incorrect sequence easier to inspect.

Before calling choices, validate that the population is nonempty, that the two lists have equal length, that every weight is finite and nonnegative, and that their sum is positive. Negative or NaN weights do not describe a useful distribution. Then call rng.choices(population, weights=weights, k=1)[0]. It returns a one-element list because choices is designed to draw any number of outcomes. The index selects the single requested result. This small wrapper gives a readable API and a place to enforce the input contract.

The final run checks a zero-weight boundary across multiple draws and rejects an all-zero weight list. It does not claim that five draws prove a three-to-one ratio; randomness needs many observations for a statistical estimate and even then individual outcomes vary. A zero-weight outcome is a deterministic invariant, which makes it a stronger unit test. If the application needs cryptographic unpredictability, use the secrets module and design that requirement separately. For ordinary reproducible simulations, pass a Random object instead of relying on hidden global state.

  • choice is uniform
  • choices uses relative weights
  • zero weight is a testable invariant
  • inject Random for repeatable simulations

See it step by step

Read the code, predict the output, then compare it with the result.

01. Zero weight blocks one outcome

PYTHON
import random
rng = random.Random(7)
print(rng.choices(['A','B'], weights=[0,1], k=5))
EXPECTED OUTPUT
['B', 'B', 'B', 'B', 'B']

Every result is B because A has zero relative weight.

02. Scale weights without changing their ratio

PYTHON
from fractions import Fraction
print(Fraction(1, 1 + 3) == Fraction(10, 10 + 30))
EXPECTED OUTPUT
True

Weights are relative; scaling both by the same positive factor preserves their ratio.

03. Reject invalid input before drawing

PYTHON
import math
weights = [1.0, float('nan')]
print(all(math.isfinite(w) and w >= 0 for w in weights))
EXPECTED OUTPUT
False

A NaN weight is not a usable nonnegative finite quantity.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

A uniform draw is a different rule

Uniform choice gives each item an equal chance. A game, simulation, or classroom experiment may instead assign different relative weights. Python's random.choices accepts a population and weights; a zero weight makes that outcome impossible when another weight is positive. We inject a random.Random instance so tests can reproduce a sequence without modifying global random state. Weights are relative values, not percentages: one and three give a one-to-three ratio, and ten and thirty describe the same distribution. This tool is for simulation, not for passwords or security tokens.

Run the five draws, then change the generator seed. The specific sequence can vary with seed, but A must remain absent. Contrast this with rng.choice, which never receives the weight list at all. Do not infer an exact distribution from only five samples.

PYTHON
import random
rng = random.Random(7)
print(rng.choices(['A','B'], weights=[0,1], k=5))
EXPECTED OUTPUT
['B', 'B', 'B', 'B', 'B']
Follow the reasoning
  1. Compare choice and choices.
  2. Predict whether A can appear.
02 / 03

Validate the weight vector

The broken run calls rng.choice even though a weight list is present. The code looks random and may pass a casual test, but it ignores the policy altogether. With an outcome weighted zero, any selection of that outcome reveals the bug decisively. Predict whether the uniform sampler can still pick A when A has weight zero in a separate list. A seeded generator makes the diagnostic repeatable, but a seed does not make an incorrect algorithm weighted. It only makes the same incorrect sequence easier to inspect.

A ratio of one to three means one quarter of the total weight belongs to the first outcome. It does not mean every fourth draw must pick that outcome. Check equal-length lists and finite nonnegative numbers before sampling.

PYTHON
from fractions import Fraction
print(Fraction(1, 1 + 3) == Fraction(10, 10 + 30))
EXPECTED OUTPUT
True
Follow the reasoning
  1. Check relative weights and lengths.
  2. Reject invalid numerical values.
03 / 03

Test a deterministic boundary

Before calling choices, validate that the population is nonempty, that the two lists have equal length, that every weight is finite and nonnegative, and that their sum is positive. Negative or NaN weights do not describe a useful distribution. Then call rng.choices(population, weights=weights, k=1)[0]. It returns a one-element list because choices is designed to draw any number of outcomes. The index selects the single requested result. This small wrapper gives a readable API and a place to enforce the input contract.

Zero is an especially useful test because it produces an exact invariant. Also test an all-zero vector and a NaN value. For cryptographic tokens, the requirements differ and random.choices is the wrong tool.

PYTHON
import math
weights = [1.0, float('nan')]
print(all(math.isfinite(w) and w >= 0 for w in weights))
EXPECTED OUTPUT
False
Follow the reasoning
  1. Use a seeded Random for repeatability.
  2. Assert zero-weight exclusion.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnMake pick_weighted use weights so A with weight zero is never returned, validate empty/mismatched/invalid weights, and return one selected item rather than a list.
Need a hint?

Check lengths and math.isfinite, require a positive total, then use rng.choices(items, weights=weights, k=1)[0].

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.What does random.choice do with a separate weights list?

    02.What can a deterministic test prove with weights [0, 1]?

    03.Why pass a Random instance into the function?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Ignoring the weights

    COMMON MISTAKE
    rng.choice(items)
    FIX
    rng.choices(items, weights=weights, k=1)[0]

    What happens: A zero-weight item can still be selected.
    choices accepts relative weights; choice selects uniformly.

    Accepting invalid totals

    COMMON MISTAKE
    rng.choices(items, weights=[0,0], k=1)
    FIX
    validate all weights and require sum(weights) > 0

    What happens: There is no positive probability mass to sample.
    Input validation makes the statistical contract explicit and error messages useful.

    Browse this lesson’s problem fixes

    Sources and further reading