I have the following code that simulates the monty hall problem (see google for more details). I used sets to do it but is there a more intuitive or efficient way to do it?

import random as r
from sets import Set
def montysim(N):
  K = 0
  for i in range(N):
    s = Set([1,2,3])
    doorstoswitch = Set([1,2,3])
    cardoor = r.randint(1,3)
    chosendoor = r.randint(1,3)
    if chosendoor != cardoor:

    montydoor = r.sample(s, 1)[0]
    newdoor = r.sample(doorstoswitch, 1)[0]
    if newdoor == cardoor:
  return float(K) / float(N)

print montysim(10000)

4 Answers 4


There are a couple of changes you can make:

  1. You can use random.choice(...) on a list, rather than random.sample(...)[0] on a set.
  2. You shouldn't use sets:

    Deprecated since version 2.6: The built-in set/frozenset types replace this module.

    This means you can change Set to just set, or instead use the syntactic sugar: {1, 2, 3}.

  3. You can make doorstoswitch at the end. You can do this by inverting the check. So rather than cardoor == r.choice(doorstoswitch) you can use cardoor not in {chosendoor, montydoor}.

  4. You can then simplify the above to just cardoor != chosendoor, as montydoor can't be the car.

  5. You can remove the sets, as there's no need for them anymore.

And so your code can be:

import random

def montysim(n):
    k = 0
    for _ in range(n):
        k += random.randrange(3) != random.randrange(3)
    return float(k) / n
  • 2
    \$\begingroup\$ I think this is a bit extreme from a readability perspective as it doesnt simulate the switching of doors etc. I would want the code to be self annotating to the point where somebody who reads it can understand what the monty hall problem is. \$\endgroup\$ Apr 13, 2017 at 0:35
  • \$\begingroup\$ @user3079275 I agree with you, but performance optimizations normally come at the cost of readability. Feel free to follow as many of my steps to achieve code you're happy with. I think to step 3 would be best for that. :) Also it could further be changed to something like: montysim = lambda n: float(sum(randrange(3) != 0 for _ in range(n))) / n, if you want to see what the extreme is. \$\endgroup\$
    – Peilonrayz
    Apr 13, 2017 at 0:47
  • 3
    \$\begingroup\$ @user3079275 I agree with you; this code wouldn't convince anybody that it was simulating the Monty Hall problem. The goal of code isn't always optimum performance! \$\endgroup\$ Apr 13, 2017 at 3:06
  • 1
    \$\begingroup\$ For instance, for even faster performance one could just change the function to return numpy.random.binomial(n, 2.0/3) / float(n) :-) It would give the same results, and much faster, but claiming that it is a simulation of the Monty Hall would be absurd (the same with the current code in this answer). \$\endgroup\$ Apr 13, 2017 at 3:33

Two things stick out to me:

First, Python has an official style-guide, PEP8. It recommends consistently using 4 spaces as indentation (you use a mix of two and four). It also recommends using lower_case_with_underscores for names.

Second, the sets module has been deprecated. You can just use the built-in set, instead of sets.Set.

import random

def monty_sim(n):
    k = 0
    for i in range(n):
        s = set([1, 2, 3])
        doors_to_switch = set([1, 2, 3])
        car_door = random.randint(1, 3)
        chosen_door = random.randint(1, 3)
        if chosen_door != car_door:

        monty_door = random.sample(s, 1)[0]
        newdoor = random.sample(doors_to_switch, 1)[0]
        if newdoor == car_door:
            k += 1
      return float(k) / float(n)

if __name__ == "__main__":
    print monty_sim(10000)

This even comes with a nice speed-boost for free!

In [2]: %timeit montysim(10000)
10 loops, best of 3: 109 ms per loop

In [4]: %timeit monty_sim(10000)
10 loops, best of 3: 54 ms per loop
  • \$\begingroup\$ Try this for a bigger speed boost: import numpy and then def montySim(n): return numpy.random.binomial(n, 2.0/3) / float(n) :-) (See the comments on the other answer.) \$\endgroup\$ Apr 13, 2017 at 7:25

I think using sets is a good approach. They're fast and powerful and often express a problem elegantly.

Set([1, 2, 3]) is more simply written as a set literal, {1, 2, 3}, which is also more efficient.

{3} is a set with one element, the number 3.

If both sides of an arithmetic expression are sets, Python will use set arithmetic. For example the minus operator implements set difference:

{1, 2, 3} - {2} == {1, 3}.

Interestingly, too, {1, 2, 3} - {4} == {1, 2, 3} i.e. the original set is unchanged. The set method call .remove(4), which seems similar, would raise a KeyError because it can't find element 4 in the set. Because your code uses .remove(), it has to perform a test before the removal to avoid a possible exception:

if chosendoor != cardoor:

The code below, which uses set difference, does not need this check.

.pop() is a useful set method. a_set.pop() means "remove any element from a_set, I don't care which, and return that element". This is a more readable, efficient and set-native way to extract a set element than using random.sample(a_set, 1)[0].

Putting set literals, set difference and .pop() together, consider the following:

switch = (doors - {revealed_goat, no_switch}).pop()

this can be read as:

the switched door is any of the three doors that is neither the revealed goat door, nor the original door chosen by the contestant (the no-switch door.)

In a real iteration, the expression might evaluate like this:

({1, 2, 3} - {1, 2}).pop() => {3}.pop() => 3

And this:

revealed_goat = (goats - {no_switch}).pop()

can be read as:

the revealed goat door is one goat door that is not the contestant's original choice (the no-switch door)

A real iteration might evaluate like this:

({1, 2} - {2}).pop() => {1}.pop() => 1

Or if no_switch correctly guesses the prize-winning car door:

({1, 2} - {3}).pop() => {1, 2}.pop() => 1 (or 2)

The last example uses the {1, 2, 3} - {4} == {1, 2, 3} set difference behaviour I mentioned earlier. The car door is 3 and the contestant's first guess is also 3. {1, 2} - {3} evaluates to {1, 2}, leaving the original set unchanged. So now there are two possible doors the Game Master could choose as the "revealed goat" door of the Monty Hall problem. .pop() will return door 1 or 2.

Importantly, we don't know which door, because sets are unordered and either door 1 or door 2 might be "popped" first. This is exactly what should happen, because in this case the Game Master has two goat doors to choose from and must choose one arbitrarily.

More about set arithmetic at https://docs.python.org/3/tutorial/datastructures.html#sets

With this set-based approach you can run about 500,000 Monty Hall simulations per second on an i7 laptop.

By the way I found a number of your variable names hard to understand, including s, chosendoor (chosen by whom?) and montydoor. You may or may not like the variable names I chose instead.

from collections import Counter
from random import choice

doors = {1, 2, 3}
doors_list = list(doors) # so we can use choice()
wins = Counter()
# car = prize door
# goats = set of non-winning doors
for monty_hall_simulation in range(10_000):
    car = choice(doors_list)
    no_switch = choice(doors_list)
    goats = doors - {car}
    revealed_goat = (goats - {no_switch}).pop()
    switch = (doors - {revealed_goat, no_switch}).pop()
    if switch == car:
        wins['switch'] += 1
        wins['no_switch'] += 1
for win_type, count in wins.most_common():
    print(f'{win_type} won {count} times')
print(f"Switching is {wins['switch'] / wins['no_switch']:.2f} times"
      " more likely to win than not switching")

Example output:

% python monty_hall.py
switch won 6681 times
no_switch won 3319 times
Switching is 2.01 times more likely to win than not switching

I think this is a bit extreme from a readability perspective as it doesnt simulate the switching of doors etc. I would want the code to be self annotating to the point where somebody who reads it can understand what the monty hall problem is.

I remember being bemused by this puzzle many years ago. And in the spirit of your quote I offer this rather hacked-together implementation. It runs through the entire game-show scenario in text before repeating it silently 10,000 times.

It's not efficient. It's not polished. It's not even particularly good, but I think it does the explaining bit. And if you squint you may not even notice the latent divide-by-zero error.

# Simulate the Monty Hall Problem

import random

class Stats:
    def __init__(self,title):
        self.title = title
        self.games = 0
        self.wins = 0
    def printit(self):
        print(self.title+": "+str(self.wins)+"/"+str(self.games)+" = " + str(self.wins*100//self.games)+"% " )

class Door:
    """ One for each door. """
    def __init__(self):
        self.status = "closed"
        self.contents = None
        self.chosen = False

class MontyHallProblem:
    """ Simulate one play of the Monty Hall problem """
    stats = None
    chosen_door = None

    def __init__(self,silent=False):
        """ Initialise the doors. """
        # this happens before the game starts, the player does not know
        # what is behind the doors. We put a car behind one door, and
        # fill the other two with goats.
        self.three_doors = [Door(), Door(), Door()]
        self.three_doors[random.randint(0,2)].contents = "FERRARI"
        for door in self.three_doors:
            if not door.contents:
                door.contents = "goat"
        if not silent:
            print("\nMonty says 'After that brief message from our sponsors WELCOME BACK to 'Let's Make a Deal'")
            print("As you can see, there are three doors in front of you ...")

    def look(self):
        """ Inspect the doors from the player's point of view. """
        for i, door in enumerate(self.three_doors):
            print("\nDoor "+str(i+1)+" is "+door.status)
            if door.status == "open":
                print("... there is a "+door.contents+" behind it!!")
            if door.chosen:
                print("... a green light shows that this is the door you have chosen")

    def choose_door(self,silent=False):
        """ Player chooses a door. """
        if silent:
            i = random.randint(0,2)
            self.chosen_door = self.three_doors[i]
            self.chosen_door.chosen = True
            i = input("\nMonty says 'Choose a door - 1, 2 or 3': ")
            while i not in "123":
                i = input("Choose a door - 1, 2 or 3: ")
            self.chosen_door = self.three_doors[int(i)-1]
            self.chosen_door.chosen = True

    def monty_opens_a_door(self, silent=False):
        """ Monty opens one of the remaining two doors, revealing ..."""
        # The exact procedure here is crucial to the outcome of the problem.
        # Some descriptions of the problem don't make it clear that Monty
        # *knows* what is behind the doors and deliberately opens one that
        # does not reveal the Grand Prize (whatever that might be)
        if not silent:
            print("\nMonty says 'Thank you for choosing a door. Your choice is shown by the ")
            print("green light. Now I am going to open one of the two remaining doors ...")
        remaining_doors = [door for door in self.three_doors if not door.chosen]
        for i, door in enumerate(remaining_doors):
            if door.contents == "FERRARI":           # so he opens the *other* door
                remaining_doors[(i+1) % 2].status = "open"
        remaining_doors[random.randint(0,1)].status = "open"

    def stick_or_switch(self,silent=False):
        """ Player chooses whether to switch choice of door. """
        global switched, non_switched
        if silent:
            switch = random.choice([True,False])
            print("\nMonty says 'There are now two doors remaining closed. You have already chosen one of them.")
            s = input("Would you like to switch your choice?' Y/N: ")
            while s not in "YyNn":
                s = input("Do you want to switch doors? Y/N: ")
            if s in ["Yy"]:
                switch = True
                switch = False

        if switch:
            self.stats = switched
            for door in self.three_doors:
                if door.status == "closed" and not door.chosen:
                    door.chosen = True
                    self.chosen_door.chosen = False
                    self.chosen_door = door
            self.stats = non_switched

    def denoument(self,silent=False):
        """ Heart-stopping stuff this. Did we win or not? """
        if not silent:
            print("\nMonty says 'You have made your choice. Come forward and open the door you have chosen.'")
            print("... You walk unsteadily towards the door and reach for the handle ...")
        self.chosen_door.status = "open"
        self.stats.games += 1
        if self.chosen_door.contents == "FERRARI":
            self.stats.wins += 1
            if not silent:
                print("\n... LIGHTS FLASH! The audience SCREAMS!! You jump for joy!!!")
            if not silent:
                print("\n... as the door swings open, you hear a disconsolate 'Baa' ...")

switched = Stats("Switched")
non_switched = Stats("Not switched")

def play_monty_hall(silent=False):
    M = MontyHallProblem(silent=silent)
    if not silent: M.look()
    if not silent: M.look()
    if not silent: M.look()
    if not silent: M.look()
    if not silent: M.look()


for i in range(10000):

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.