skip to content

Why does `deck = random.shuffle(deck)` set deck to None, and what returns a shuffled copy?

level: middleimportance: should knowfreq 40%

answer

  1. In-place operations return nothing in Python
  2. Same rule as list.sort
  3. Swaps through indices, so mutable only
  4. Ask sample for a new list
  5. k equals the length gives a shuffled copy

basics

~10 s

random.shuffle reorders a mutable sequence in place and returns None, following Python's convention for mutating methods. For a shuffled copy, call random.sample(deck, k=len(deck)), which returns a new list and leaves the original untouched.

solid answer

~40 s

`random.shuffle(x)` performs a Fisher-Yates swap loop over the sequence's own indices, so it mutates the argument and — like `list.sort()` and `list.reverse()` — returns `None` to signal that the work happened in place. Rebinding the name to the result therefore throws the shuffled list away. When you need a new list, `random.sample(population, k=len(population))` returns a fresh shuffled selection without replacement and leaves the input alone. Related choices in the same family: `random.choice(seq)` returns one element and raises `IndexError` on an empty sequence; `random.sample()` draws without replacement and raises `ValueError` if `k` exceeds the population; `random.choices()` draws *with* replacement and supports weights. Because `shuffle()` assigns through indices it needs a mutable sequence — a tuple raises `TypeError` — and since Python 3.11 `sample()` refuses sets, telling you to pass `sorted(s)`.

code

pycon · 8 lines
pycon
>>> import random
>>> deck = [1, 2, 3, 4]
>>> print(random.shuffle(deck), deck)
None [3, 1, 2, 4]
>>> random.sample(deck, k=len(deck))
[2, 4, 1, 3]
>>> deck
[3, 1, 2, 4]

go deeper

for a junior

Recall that random.shuffle() changes the list you pass and gives back None, so never assign its result, and that random.sample() is what returns a new list.

for a middle

Explain the in-place convention shared with list.sort(), why an index-swapping shuffle needs a mutable sequence, and when to reach for sample versus choices.

for a senior

Watch for the operational edges in review: random.choice() on an empty sequence crashing a job, and a seeded sample over a set or other unordered input that quietly fails to reproduce.

for a principal

Frame reproducibility as needing both a fixed generator state and a deterministic input ordering, and make that a review expectation wherever sampled data feeds a report someone compares over time.

## The None is a convention, not a bug Python consistently marks in-place operations by returning `None`: `list.sort()`, `list.reverse()`, `list.append()`, `dict.update()` and `random.shuffle()` all do it. The reason is to make the mutation impossible to miss — if these returned the object, `b = a.sort()` would leave `a` and `b` aliasing one mutated list, and readers would have to remember which functions copy and which do not. Returning `None` makes the wrong idiom fail loudly on the next line instead. So: ```pycon >>> import random >>> deck = [1, 2, 3, 4] >>> random.shuffle(deck) >>> deck [3, 1, 2, 4] >>> deck = random.shuffle(deck) >>> deck is None True ``` The shuffle really happened both times; the second line simply discarded the list by rebinding the name. ## How shuffle does its work `shuffle()` walks the sequence from the end down, and for each position `i` picks an index `j` in `0..i` and swaps `x[i]` with `x[j]`. That is the modern Fisher-Yates shuffle, uniform over all permutations, linear in the length, and requiring no extra storage. Two consequences fall out of "it assigns through indices": - The argument must be a **mutable sequence**. A `list` or a `bytearray` works; a tuple raises `TypeError: 'tuple' object does not support item assignment`, and anything that only supports iteration has no indices to swap. - The uniformity claim is bounded by the generator's period. MT19937 has an enormous but finite state, so it cannot reach every permutation of a long sequence — a well-known theoretical limit that matters for combinatorics research and essentially never for application code. An older signature detail worth knowing if you read legacy code: `shuffle()` used to accept a second argument, a zero-argument function returning a float in `[0.0, 1.0)`. It was deprecated in 3.9 and **removed in 3.11**, so `random.shuffle(x, myrandom)` now raises `TypeError` on any current interpreter. The replacement is to call `shuffle()` as a method of your own generator: `rng.shuffle(x)`. ## Getting a copy instead When the input must survive — it is someone else's list, or you want the original ordering for a report — `random.sample()` is the copy-returning form: ```pycon >>> import random >>> deck = [1, 2, 3, 4] >>> random.sample(deck, k=len(deck)) [2, 4, 1, 3] >>> deck [1, 2, 3, 4] ``` `sample(population, k)` selects `k` distinct *positions* without replacement and returns them as a new list in selection order, so with `k = len(population)` it is a shuffled copy. It raises `ValueError` when `k` is larger than the population. Note the wording: distinct positions, not distinct values — if the input has duplicates, the output can too. The alternative when you explicitly want an unchanged original and no algorithmic subtlety is simply `copy = deck[:]` followed by `random.shuffle(copy)`; that is often the clearest thing to write, and it makes the copy visible to the reader. ## Picking the right member of the family A quick decision table for the selection helpers, all of which draw from the same generator: - **`random.choice(seq)`** — one element, uniformly. Raises `IndexError` on an empty sequence, which is the usual production crash when an upstream filter returned nothing. - **`random.sample(population, k)`** — `k` elements **without** replacement, new list, original untouched. Accepts a `counts` keyword to describe a population with repeats compactly. - **`random.choices(population, weights=None, k=1)`** — `k` elements **with** replacement, and the only one of the three that takes weights. Returns a list even when `k` is 1. - **`random.shuffle(x)`** — reorder in place, return `None`. One container gotcha ties the group together. Since **Python 3.11**, `random.sample()` no longer accepts a set: it raises `TypeError: Population must be a sequence. For dicts or sets, use sorted(d).` The reason is that a set has no defined order, so sampling from it was never reproducible across runs even under a fixed seed — the same seed could pick different elements. Converting with `sorted()` imposes a deterministic order first, which is what makes a seeded sample repeatable. It is a good illustration of the leaf's underlying theme: reproducibility needs a fixed starting state *and* a fixed input ordering. ## The distribution helpers sit alongside them `random.gauss(mu=0.0, sigma=1.0)` and its siblings draw from that same generator, so they interleave with `shuffle()` and `sample()` in one stream and are covered by the same seed. `gauss()` has one quirk worth knowing: it produces normal deviates in pairs and keeps the spare for the next call, so two consecutive calls consume different amounts of underlying state, and it carries a little instance state beyond the twister. Both arguments have defaults, so `random.gauss()` with no arguments is a standard normal draw.

  • What is the difference between random.sample and random.choices?
    `sample(population, k)` draws `k` distinct positions **without** replacement, so no position is picked twice and `k` may not exceed the population size. `choices(population, weights=None, k=1)` draws **with** replacement, so repeats are expected, `k` can be any size, and it is the only one of the two that accepts weights. Both return a new list and leave the input untouched.
  • Why does random.sample refuse a set, and what should you pass instead?
    Since Python 3.11 it raises `TypeError` because a set has no defined order, so a seeded sample from one was never reproducible — the same seed could yield different elements between runs. Pass `sorted(s)` (or any list you build deterministically) to fix the ordering first; the error message says so explicitly.
  • How do you shuffle using your own generator rather than the module-level one?
    Call `shuffle()` as a method: `rng = random.Random(seed)` then `rng.shuffle(x)`. The old route — passing a custom random function as `random.shuffle(x, func)` — was deprecated in 3.9 and removed in 3.11, so it now raises `TypeError`. The method form is also what you want for per-thread or injected generators.

saying these in an interview costs you the question

  • Expects random.shuffle to return the shuffled list
  • Thinks shuffle copies rather than mutating the argument
  • Tries to shuffle a tuple or a plain iterable
  • Confuses sample (no replacement) with choices (with replacement)
  • Passes a set to random.sample and expects a seeded result to repeat
  • Calls random.choice on a possibly-empty sequence without handling IndexError

context