Categories
Mac admin'ing

Using AutoPkg to distribute and test Munki 4 (or any new Munki version) in prerelease

10 December, 2019 update: Munki 4 is now an official (not pre-) release, so the below instructions are for when the next version is in prerelease…

25 November, 2019 (original post)
As of this writing, Munki 4 is in the late stages of testing (its current on release candidate 1), so if you want to be part of the testing process and distribute it to part of your fleet, you want to start by first importing it into your Munki repo.

There is a new AutoPkg recipe for this called munkitools4.munki.recipe

First, make the override for it:

autopkg make-override munkitools4.munki

Then, edit the override, so it includes prereleases by adding in the appropriate true to the override’s XML:

<key>Input</key>
<dict>
<key>INCLUDE_PRERELEASES</key>
<string>true</string>

Then, run it as you normally would, just to verify it works (before including it in your scheduled runs):

autopkg run -v munkitools4.munki MakeCatalogs.munki

Categories
Mac admin'ing

Forcing updates to Google Chrome using Chrome preferences / a Chrome profile

Why use Chrome relaunch notification instead of Munki

I’m a huge fan of using Munki to patch software on macOS, but Munki is generally polite—it usually won’t kill an application while the user is using it. There is an option in Munki to force an install after a certain date, but that will log the user out of her computer completely in order to install whatever item you set as a forced install.

The actual preferences for Chrome relaunch notification

If you want to just have Chrome update to the latest version without logging out the user, but you want to force that to happen (say, if there’s a fairly serious zero-day security vulnerability), you can use Chrome preferences to do so:

defaults write com.google.Chrome RelaunchNotification -int 2

defaults write com.google.Chrome RelaunchNotificationPeriod -int 3600000

The first command forces a relaunch instead of just recommending one. The second one gives the period of time (in milliseconds) the user has before the relaunch happens.

Using a profile instead of commands to manage the preferences

Those commands probably aren’t something you want to run at scale. You’d want to use that as a quick test, and then you could deploy those settings as a .mobileconfig profile, which you can use mcxToProfile to do. You can also apparently use ProfileCreator to do so as well (using ProfileManifests), but I haven’t fully explored that yet. And, if you’re a Jamf user, you can use plutil -convert xml1 to use custom settings for a Jamf-created profile.

Deploy (and undeploy) thoughtfully

I’m going to give a major caveat that, unless you want to perpetually annoy your users, you probably don’t want to have this profile installed all the time, because it means every single time there’s a Chrome update (and Google does update Chrome quite frequently), your users will have only an hour (or whatever time period you set for RelaunchNotificationPeriod) to relaunch Chrome. Not all Chrome updates are immediately critical, so use this obnoxious “you must relaunch” Chrome policy judiciously.

What your users will see

This is the type of warning users will see:

Will users lose their open tabs?

I can’t say definitively, because the functionality may change in the future, but as of the writing of this blog post (late November, 2019), even if users have not set Continue where you left off, this forced relaunch will, in fact, re-open the tabs that were open before:

Um, test for yourself, obviously. You don’t want to have a bunch of angry users. You could, alternatively, have your .mobileconfig profile manage the RestoreOnStartup preference.

What’s the actual user experience like for the restart?

In my testing, it seems the 6-minute warning is the last one you can dismiss, and then when it’s ready to relaunch to install the update, Chrome gives absolutely no notification that the relaunch is happening when it happens. Chrome just closes out and then relaunches.

Further reading

More details at Google’s Notify users to restart to apply pending updates.

Even more technical details at Chrome Enterprise policy list.

Categories
Mac admin'ing

Setting up Reposado without downloading Apple update pkgs

Reposado allows you to set up your own local repo of Apple software updates. This can be handy if you want to control the flow of updates (having a testing branch, for example, and then promoting items from testing to production).

With Apple deprecating custom catalog URLs in Catalina (they still work for now, though), you may not want to actually create a full Reposado repo. Maybe you just want a way to check for product keys without replicating the entire repo, including pkgs.

It’s fairly simple to do this:

git clone https://github.com/wdas/reposado
sudo mkdir -p /usr/local/reposado
sudo cp -R reposado/code/* /usr/local/reposado/
sudo mkdir -p /Library/Application\ Support/reposado/html
sudo mkdir -p /Library/Application\ Support/reposado/metadata

This is the key part, when you go to configure Reposado, you want the Base URL to be blank:

sudo /usr/local/reposado/repoutil --configure
Filesystem path to store replicated catalogs and updates [/Library/Application\ Support/reposado/html]: /Library/Application Support/reposado/html

Filesystem path to store Reposado metadata [/Library/Application\ Support/reposado/metadata]: /Library/Application Support/reposado/metadata

Base URL for your local Software Update Service
(Example: http://su.your.org -- leave empty if you are not replicating updates) []:

Then, you can run

sudo /usr/local/reposado/repo_sync

and the total download should be only about 150 MB (as opposed to hundreds of GB if you were to download all the pkgs as well).

Categories
Mac admin'ing

Munki hack: force uninstall after a certain date

Munki has an option to force install by a certain date (specifically, using the force_install_after_date key), but it doesn’t do a force uninstall after a certain date.

You can make an item a managed uninstall, which means Munki will uninstall it when possible, but if that requires a logout or reboot, you don’t know when that might happen.

So to force an uninstall after a certain date, there’s a messy hack you can employ, which is to create an uninstall nopkg.

So, for example, if you want to force remove item X, you would have, of course, an item X in your repo, but you would then create a nopkg item for uninstall-X, which, when “installed” would uninstall X.

Now, this can be a bit tricky, because Munki has built-in logic for how it does its installs. For example, let’s say item X had a relationship with item Y, where item Y is an update for X. If you use Munki’s built-in installation logic to make item X a managed uninstall, Munki would know to remove item Y as well. But if you create a separate nopkg that just runs some script to remove item X, the nopkg doesn’t also necessarily know item Y is an update for item X.

Likewise, if you have item X as a managed install as well, you could have Munki fighting with itself, since it will try to install item X but also (via your hacky nopkg) install item uninstall-X. (In ordinary circumstances, make an item both a managed install and a managed uninstall would usually have Munki just keep it installed.)

But if you are careful with how you construct things, this can definitely work. I wouldn’t recommend it (better to just let managed uninstalls do its job), but if you absolutely need to remove a certain piece of software by a certain date, this can do it.

Another potential hack is to have item Z force install by that same date, and then make item X a regular managed uninstall, and when users are forced to install item Z, item X will uninstall at the same time.

Categories
Mac admin'ing

Using Munki to ignore Catalina upgrade in macOS

Apple used to make you go out of your way to download an OS upgrade. Then, Apple started having those OS upgrade installers auto-download to the /Applications folder. Then Apple made it so OS upgrades appeared as regular updates.

For Mac admins who aren’t ready to have their clients upgrade to Catalina (and potentially have a lot of things break), there is a way to tell the client machine to ignore the update and thus not advertise it to the user.

For those who are Munki admins, I’ve created a nopkg that will allow you to “install” ignoring the Catalina upgrade (but you can easily tweak the script to ignore any update) and also “uninstall” ignoring the Catalina upgrade (or any update).

Note: If you “ignore” Catalina, you can still find it in the Mac App Store, but if you try to install it, it will open up System Preferences and say it can’t be found. So “ignoring” actually means “disabling.” That doesn’t stop you from using Munki to install an upgrade, though.

Categories
Mac admin'ing

Scripting changing the user picture in macOS

Sometimes, you may want to programmatically set a default user picture (with the option for the user to change it later to a picture of her own choice).

You used to be able to delete the JPEGPhoto attribute and just add in a Picture attribute, but that seems to have broken somewhere between October 2018 and September 2019.

In macOS 10.14.6, if you use dscl to delete the JPEGPhoto attribute, and then add in a Picture attribute, you’ll see one of the user pictures change, but the other one will default to the generic silhouette:

Unfortunately, you also can’t just script writing in the hex of the photo via a dscl . -create /Users/username JPEGPhoto ALLOFTHEHEXTEXT command. You will get a /usr/bin/dscl: Argument list too long error.

Someone on Stack Exchange managed to find a solution using dsimport.

Note: I did some further testing, and you may need to first run

sudo dscl . delete /Users/username JPEGPhoto
sudo dscl . delete /Users/username Picture

before successfully running the script linked to below.

If you run that script, you’ll see it changes both photos.

This script works for now in 10.14.6, and it also works in Catalina beta build 19A578c.

Categories
Begin Python

The 12 Days of Christmas… or any countdown song

Loops are a big part of programming, because one of the benefits of programs is that they can automate repetitive tasks for humans. If we were to type out the complete lyrics to “The 12 Days of Christmas,” we would end up having to do a lot of typing! And it wouldn’t be fun typing—it would be boring, repetitive typing.

What about if you were to tell someone the lyrics to “The 12 Days of Christmas”? How would you describe it without repeating yourself? That’s pretty much how this program works.

So if I were to describe in English how to sing the song, I would say “There are 12 days. Each day has its own set of gifts for that day. You start with the gifts for the 1st day. Then you go to the 2nd and say the gifts for the 2nd day and the 1st day. Then you keep going to the 3rd day and 4th day all the way until the 12th day, when you count down all the gifts until you get back down to the 1st day.” Even that would be a bit confusing. Fortunately, there are easier ways to give the instructions in Python.

You may remember dictionaries from the rock, paper, scissors game. We’re going to use a different type of dictionary for this, though, because normal Python dictionaries don’t stay in order, and the order is critical for the 12 Days of Christmas.

I’m going to give you a bunch of code to type in (yes, as always better to retype than copy/paste if you can), but I’m also going to explain along the way what’s happening:

# We need to import OrderedDict from the collections module, because dictionaries in Python usually aren't ordered
from collections import OrderedDict

# Create the ordered dictionary that pairs a day of Christmas with the set of gifts for that day
days_gifts=OrderedDict([("1st" , "A partridge in a Pear Tree"),
                        ("2nd" , "Two Turtle Doves"),
                        ("3rd" , "Three French Hens"),
                        ("4th" , "Four Calling Birds"),
                        ("5th" , "Five Golden Rings"),
                        ("6th" , "Six Geese a Laying"),
                        ("7th" , "Seven Swans a Swimming"),
                        ("8th" , "Eight Maids a Milking"),
                        ("9th" , "Nine Ladies Dancing"),
                        ("10th" , "Ten Lords a Leaping"),
                        ("11th" , "Eleven Pipers Piping"),
                        ("12th", "12 Drummers Drumming")])

We could use a for loop to go through the dictionary, but since we have to know at all times where we are (for example, if you’re on the 4th day, you have to know that 3rd is the next day, etc.), we’ll keep a counter to know exactly what part of the dictionary we’re in.

We’re going to use a variable called counter that will store what number we’re on. Remember that the first item in the list is item number 0, not item number 1. Let’s just do a simple loop to go through each of the days from the 1st day to the 12th day:

# We need to import OrderedDict from the collections module, because dictionaries in Python usually aren't ordered
from collections import OrderedDict

# Create the ordered dictionary that pairs a day of Christmas with the set of gifts for that day
days_gifts=OrderedDict([("1st" , "A partridge in a Pear Tree"),
                        ("2nd" , "Two Turtle Doves"),
                        ("3rd" , "Three French Hens"),
                        ("4th" , "Four Calling Birds"),
                        ("5th" , "Five Golden Rings"),
                        ("6th" , "Six Geese a Laying"),
                        ("7th" , "Seven Swans a Swimming"),
                        ("8th" , "Eight Maids a Milking"),
                        ("9th" , "Nine Ladies Dancing"),
                        ("10th" , "Ten Lords a Leaping"),
                        ("11th" , "Eleven Pipers Piping"),
                        ("12th", "12 Drummers Drumming")])

# Initialize a counter starting at beginning of the list
counter=0

# Loop through each of the days until we get to 0 from 11
while counter < len(days_gifts):
 
   # Get the actual day from the ordered dictionary
   current_day=days_gifts.keys()[counter]
 
   # Print the intro to the day
   print "On the %s day of Christmas, my true love gave to me:" % (current_day)

   # Print the gift given
   print "%s," % days_gifts.values()[counter]
 
   # Increase first counter to get closer to the end of the list (the 12th day of Christmas)
   counter+=1

If you run that and all goes well, you should see 12 days of Christmas and what was given on each day.

But that’s just a start. That’s not how the song works. You get way more presents than that!

# We need to import OrderedDict from the collections module, because dictionaries in Python usually aren't ordered
from collections import OrderedDict
 
# Create the ordered dictionary that pairs a day of Christmas with the set of gifts for that day
days_gifts=OrderedDict([("1st" , "A partridge in a Pear Tree"),
                        ("2nd" , "Two Turtle Doves"),
                        ("3rd" , "Three French Hens"),
                        ("4th" , "Four Calling Birds"),
                        ("5th" , "Five Golden Rings"),
                        ("6th" , "Six Geese a Laying"),
                        ("7th" , "Seven Swans a Swimming"),
                        ("8th" , "Eight Maids a Milking"),
                        ("9th" , "Nine Ladies Dancing"),
                        ("10th" , "Ten Lords a Leaping"),
                        ("11th" , "Eleven Pipers Piping"),
                        ("12th", "12 Drummers Drumming")])

# Initialize a counter starting at beginning of the list
counter=0

# Loop through each of the days until we get to 0 from 11
while counter < len(days_gifts):
 
   # Get the actual day from the ordered dictionary
   current_day=days_gifts.keys()[counter]
 
   # Print the intro to the day
   print "On the %s day of Christmas, my true love gave to me:" % (current_day)
 
   # Set up a second counter, because we do a loop within a loop until we get to the partridge in a pear tree.
   # For now, we'll make the second counter the same as the first
   second_counter=counter

   # The second counter is going to move in the other direction.
   # While the first counter is going from the beginning of the dictionary to the end of the dictionary,
   # the second counter is going from the current place to the beginning of the dictionary.
   while second_counter >= 0:

      # Print what gifts are for the second counter
      print "%s," % days_gifts.values()[second_counter]
 
      # Decrease the second counter so we get a little closer to the beginning of the list (the 1st day of Christmas)
      second_counter-=1
      
   # Increase first counter to get closer to the end of the list (the 12th day of Christmas)
   counter+=1

Okay. Why do we need a second counter? Well, think about the 4th day of Christmas, as an example. For the 4th day, we need to keep track of being on the 4th day (so the next time we sing the song, we’ll know to go to the 5th day)—that’s the regular counter variable, but then we also need to count down to the 3rd day, the 2nd day, and then the 1st day—that would be the second_counter.

Notice a problem when you run this? There is supposed to be an “And” in there if it’s not the first day to end with the partridge in a pear tree. There also is a comma at the end of the last gift every time.

# We need to import OrderedDict from the collections module, because dictionaries in Python usually aren't ordered
from collections import OrderedDict
 
# Create the ordered dictionary that pairs a day of Christmas with the set of gifts for that day
days_gifts=OrderedDict([("1st" , "A partridge in a Pear Tree"),
                        ("2nd" , "Two Turtle Doves"),
                        ("3rd" , "Three French Hens"),
                        ("4th" , "Four Calling Birds"),
                        ("5th" , "Five Golden Rings"),
                        ("6th" , "Six Geese a Laying"),
                        ("7th" , "Seven Swans a Swimming"),
                        ("8th" , "Eight Maids a Milking"),
                        ("9th" , "Nine Ladies Dancing"),
                        ("10th" , "Ten Lords a Leaping"),
                        ("11th" , "Eleven Pipers Piping"),
                        ("12th", "12 Drummers Drumming")])

# Initialize a counter starting at beginning of the list
counter=0

# Loop through each of the days until we get to 0 from 11
while counter < len(days_gifts):
 
   # Get the actual day from the ordered dictionary
   current_day=days_gifts.keys()[counter]
 
   # Print the intro to the day
   print "On the %s day of Christmas, my true love gave to me:" % (current_day)
 
   # Set up a second counter, because we do a loop within a loop until we get to the partridge in a pear tree.
   # For now, we'll make the second counter the same as the first
   second_counter=counter

   # The second counter is going to move in the other direction.
   # While the first counter is going from the beginning of the dictionary to the end of the dictionary,
   # the second counter is going from the current place to the beginning of the dictionary.
   while second_counter > 0:

      # Print what gifts are for the second counter
      print "%s," % days_gifts.values()[second_counter]
 
      # Decrease the second counter so we get a little closer to the beginning of the list (the 1st day of Christmas)
      second_counter-=1
      
   # Print the partridge in a pear tree by itself
   if counter == 0:
      print "%s\n" % days_gifts.values()[second_counter]
   else:
      print "and\n%s\n" % days_gifts.values()[second_counter]
 
   # Increase first counter to get closer to the end of the list (the 12th day of Christmas)
   counter+=1

Two changes need to happen for that to occur.

First, the inner while loop will end when it’s greater than 0, not greater than or equal to 0, so we do a special case for the last one (the partridge in a pear tree).

We also add in a little if/else at the end. With this if/else logic we’re just saying “If it’s the 1st day, don’t put in the ‘and”; otherwise, put in the ‘and.'”

Now, if you run it, it should work as you would normally sing the song.

Categories
Begin Python

Generating a random passphrase

Disclaimer

I’m not going to go into all the politics of passwords. Users are always trying to balance security with convenience. The point of this tutorial is not to say “This is how you generate the best password.” It’s more like “If you want to generate a random passphrase, this is one way to do it in Python.” The passwords you generate with this won’t be stuff like 6ZFtK!’z[^R88Rgd, but they also won’t be password or 123456.

Creating and running the file

Open up your text editor and save the empty text file as PassphraseGenerator.py on your desktop. In the text editor type in

print "This Python script runs!"

and run it.

If you ran the code correctly, you should see This Python script runs! appear after you hit Enter.

Any time you make changes to your script, you’ll have to run it again to see the changes. If you’re use PyCharm, you can just run it again. If you’re using a text editor, you’ll have to save your changes first and then run it again.

Getting the words file contents

On macOS (and most Linux distributions), there’s a file that has a dictionary of words. It’s usually located at /usr/share/dict/words, which is a hidden directory of sorts (you won’t find it by just opening up your file browser and clicking around).

So we’re going to check that the file exists and then display its contents.

First, import the os module:

import os

all this does is allow us to access other files on the computer. Now we’re going to say what file we want to check for and then check to see if the file exists:

import os

# This is where it is for macOS and Linux. You may have to adjust this path for Windows or other operating systems.
words_location = '/usr/share/dict/words'

# Check to see if the location exists
if os.path.isfile(words_location):

   # Get the words into a list we can use
   with open(words_location) as file:
      words_list = file.read().splitlines()

   # Show the list
   print words_list

else:
   print "%s does not exist, so we can't get words out" % words_location

Remember that indents (with spaces, not tabs) are important and that you can copy and paste code, but I highly recommend you retype the code to learn it a bit better.

We can use os.path.isfile() because we imported the os module earlier. All that does is check “Is there a file at this location?”

The next set of lines just opens the file as variable file and then puts each read line in file into a list called words_list.

Once each word is in the list, we just print out the whole list (which is long!) just so you can see what the word looks like.

If the file location does not exist, we just have a message to say “Hey, there is no /usr/share/dict/words file.” Sad trumpet.

Go ahead and run your Python script as you did before. You should get a list of all the words in the dictionary.

Getting a random word from the dictionary

Okay. We don’t actually want the entire dictionary, we just want random words. Before we get random words (plural), though, let’s just start with one random word (singular).

So just as we needed the os module to access files on the computer, we’ll need to import the random module to get random selections.

import os
import random

# This is where it is for macOS and Linux. You may have to adjust this path for Windows or other operating systems.
words_location = '/usr/share/dict/words'

# Check to see if the location exists
if os.path.isfile(words_location):

   # Get the words into a list we can use
   with open(words_location) as file:
      words_list = file.read().splitlines()

   # Pick a random word
   random_word = random.choice(words_list).title()

   # Print the random word
   print random_word

else:
   print "%s does not exist, so we can't get words out" % words_location

We’re also, instead of printing out the whole list, going to select a random choice from the words list, put in title case (so Otters instead of otters, for example), and then print that random word out.

Try it out. In fact, run your script a few times so you can see how a (probably different) random word is picked every time.

Creating a passphrase instead of a word

So instead of one random word, we actually want to have a couple or several random words. Let’s set a minimum length for how long this “phrase” is going to be and keep building the phrase until it gets to (or beyond) that length.

import os
import random

# This is where it is for macOS and Linux. You may have to adjust this path for Windows or other operating systems.
words_location = '/usr/share/dict/words'

# Minimum number of characters the "phrase" should be
passphrase_min_length = 14

# Check to see if the location exists
if os.path.isfile(words_location):

   # Get the words into a list we can use
   with open(words_location) as file:
      words_list = file.read().splitlines()

   # Initialize a blank passphrase we'll add to
   passphrase = ''
   
   # Keep adding random words to the passphrase until it's as long as the minimum length
   while len(passphrase) < passphrase_min_length:

      # Pick a random word
      random_word = random.choice(words_list).title()

      passphrase += random_word

   print passphrase

else:
   print "%s does not exist, so we can't get words out" % words_location

Note that in addition to setting a minimum length, we're also setting a blank passphrase to start with, because the while loop just keeps adding to the passphrase (that’s what the += sign means), and you can’t add to something that doesn’t exist, so we have to start with blank and then keep adding to it.

Adding spaces between words

What if we want to add some spaces? Well, we can do that by adding a space and the random word to the passphrase instead of just the random word:

import os
import random

# This is where it is for macOS and Linux. You may have to adjust this path for Windows or other operating systems.
words_location = '/usr/share/dict/words'

# Minimum number of characters the "phrase" should be
passphrase_min_length = 14

# Check to see if the location exists
if os.path.isfile(words_location):

   # Get the words into a list we can use
   with open(words_location) as file:
      words_list = file.read().splitlines()

   # Initialize a blank passphrase we'll add to
   passphrase = ''
   
   # Keep adding random words to the passphrase until it's as long as the minimum length
   while len(passphrase) < passphrase_min_length:

      # Pick a random word
      random_word = random.choice(words_list).title()

      passphrase += ' ' + random_word

   print passphrase

else:
   print "%s does not exist, so we can't get words out" % words_location

Go ahead and run that. Do you see the problem? There's a space before the first word. We want spaces between the words but not before the first word, so we'll have to put a little if/then logic in there: If it’s the first time we’re running this (i.e., the passphrase is blank right now), let’s not put in the space. Otherwise, let’s put in the space.

import os
import random

# This is where it is for macOS and Linux. You may have to adjust this path for Windows or other operating systems.
words_location = '/usr/share/dict/words'

# Minimum number of characters the "phrase" should be
passphrase_min_length = 14

# Check to see if the location exists
if os.path.isfile(words_location):

   # Get the words into a list we can use
   with open(words_location) as file:
      words_list = file.read().splitlines()

   # Initialize a blank passphrase we'll add to
   passphrase = ''
   
   # Keep adding random words to the passphrase until it's as long as the minimum length
   while len(passphrase) < passphrase_min_length:

      # Pick a random word
      random_word = random.choice(words_list).title()

      # Check to see if this the first time through the loop
      if passphrase != '':
         # If it's not the first time, add a space
         passphrase += ' '
      
      # Regardless of whether it's the first time or not, add the random word
      passphrase +=  random_word

   print passphrase

else:
   print "%s does not exist, so we can't get words out" % words_location

Now if you run it, you should see a space between words but not before the first word. Also try running it with a longer minimum than 14.

Extra Challenges

If your mastery over this exercise is pretty good, try to challenge yourself with a few tweaks:

  • Make it so the random words aren’t too long? (It’s up to you to decide what “too long” means.)
  • Generate multiple passphrases at once instead of only one phrase at a time?
  • Ask the user for input on what parameters she’d like to change (the maximum length, etc.).
Categories
Begin Python

Making a Rock, Paper, Scissors game in Python

Before you code, figure out what you’re trying to do with the code
A brief note on code
Using comments in code
Expanding shorthand logic
Considering another approach
No one right way to approach problems
Let’s actually start writing some code
Don’t trust user input
Picking a choice at random
Comparing values with if/elif/else
Extra Credit
What if we want to keep playing and not have it start over again all the time?
Debugging (finding/fixing) Errors
Super extra credit
Still other approaches
Whoa! That was a lot!

Before you code, figure out what you’re trying to do with the code

Let’s create a game. In real life, it works like this: there are two players—each player simultaneously picks one of three options: rock, paper, or scissors. If the two players pick rock and paper, paper wins (presumably by enveloping the rock?). If the two players pick paper and scissors, scissors wins (by cutting the paper). If the two players pick rock and scissors, rock wins (by smashing the scissors). If the two players pick the same option, it’s a tie, and they have to play again.

What I’ve just described above is the absolute first thing you should do before you start a programming project. You don’t start by writing code—you start by understanding what you’re trying to achieve with the code. Generally speaking, when you code, you’ll either code for yourself (in which case, it can be helpful to you to articulate your actual goals and workflow) or you code for someone else (in which case, you don’t even know what to code unless that person explains to you what the process should be).

You can draw a visual or just write out the process in plain English, but understanding the flow and logic is far more important than actual programming code.

A brief note on code

Even though you can copy and paste code into whatever text editor you’re using, you will learn better if you retype instead of copy/paste, so I’d highly recommend you retype the code you see. Mistyping or misspelling things is part of the learning process, so don’t fret if you get an error message.

The nice thing is that python error messages will usually tell you exactly what line of code the error appears in.

Using comments in code

So one way that you can approach writing out your process is to start with comments (“code” that doesn’t do anything).

# Person one "plays" rock, paper, or scissors

# Person two "plays" rock, paper, or scissors

# We have to determine which person won or if there was a tie

# We have to say who won

To make things a bit simpler, we’re actually going to have only one player and then the opponent will just be the Python program itself, so the comments will look a bit more like this:

# The user (person) "plays" rock, paper, or scissors

# The computer (program) "plays" rock, paper, or scissors

# We have to determine whether the user won or if there was a tie

# We have to say whether the user won or lost

Expanding shorthand logic

We have to determine whether the user won or if there was a tie is a bit too vague. How do we determine that? Well, one way we can do it is a bit cumbersome, with a series of if/then statements:

# The user (person) "plays" rock, paper, or scissors

# The computer (program) "plays" rock, paper, or scissors

# We have to determine whether the user won or if there was a tie
   # If the user played rock and the computer played rock, then it's a tie
   # If the user played rock and the computer played paper, then the computer wins
   # If the user played rock and the computer played scissors, then the user wins
   # If the user played paper and the computer played rock, then the user wins
   # If the user played paper and the computer played paper, then it's a tie
   # If the user played paper and the computer played scissors, then the computer wins
   # If the user played scissors and the computer played rock, then the computer wins
   # If the user played scissors and the computer played paper, then the user wins
   # If the user played scissors and the computer played scissors, then it's a tie

# We have to say whether the user won or lost

But now we have a different problem here—a long list of if/then statements, which maybe could be simpler.

Considering another approach

What if we assigned a number to each option? Rock is 0, paper is 1, and scissors is 2.

If we do that, look at each of these scenarios:

   # If the user played rock and the computer played rock, then it's a tie
      # 0 - 0 = 0
   # If the user played rock and the computer played paper, then the computer wins
      # 0 - 1 = -1
   # If the user played rock and the computer played scissors, then the user wins
      # 0 - 2 = -2
   # If the user played paper and the computer played rock, then the user wins
      # 1 - 0 = 1
   # If the user played paper and the computer played paper, then it's a tie
      # 1 - 1 = 0
   # If the user played paper and the computer played scissors, then the computer wins
      # 1 - 2 = -1
   # If the user played scissors and the computer played rock, then the computer wins
      # 2 - 0 = 2
   # If the user played scissors and the computer played paper, then the user wins
      # 2 - 1 = 1
   # If the user played scissors and the computer played scissors, then it's a tie
      # 2 - 2 = 0

It should be fairly obvious that if the user and computer pick the same number, subtracting one from the other will leave you with 0.

But also notice that if the user wins, it’s either -2 or 1. If the computer wins, it’s either 2 or -1.

So that makes the logic a lot simpler.


   # Find the numeric equivalent of what the user plays
   # Find the numeric equivalent of what the computer plays
   # Find the difference between the two
   # If the difference is 0, it's a tie
   # If the difference is -2 or 1, the user wins
   # Any other scenario, the computer wins

Why does this work? Well, think about it. If you list out the three possibilities in the Rock, Paper, Scissors order, each possibility is one higher number than the previous one. Rock, 0; Paper, 1. Paper is one more than Rock, so Paper wins. Paper, 1; Scissors, 2. Scissors is one more than Paper, so Scissors wins. Rock isn’t 1 more than Scissors (unless you consider the first to be one higher than the last), but it is 2 less than Scissors (-2).

No one right way to approach problems

We’ve basically gone from 9 separate (and extremely repetitive) if/then statements down to 3 if/then statements. This is really the tough part of programming—not the actual code you write but your approach to problem-solving.

There are distinct advantages to the latter approach (fewer steps, less repetition) and also advantages to the former approach (less abstraction—you don’t have to arbitrarily assign numbers to rock, paper, and scissors). There is no one right way to approach things, but you should be thinking about all the pros and cons.

Another advantage the latter has over the former is flexibility. What if, all of a sudden, the rules changed and rock beat paper, which beat scissors, which beat rock? In the former approach, you’d have to rewrite six of the nine if/then statements. In the latter approach, all you’d have to do is reassign the numbers as rock 2, paper 1, scissors 0 instead of rock 0, paper 1, scissors 2… or pick different winning numbers.

Let’s actually start writing some code

The first thing we want to do is define what the available choices are, so in your text editor or online editor, type in (again, you can technically copy and paste, but typing will help you get used to the syntax of programming):

choices = { "Rock": 0, "Paper": 1, "Scissors": 2 }

All this does is just define what the available choices are and assign a number to the choice.

Now, let’s get the user to pick one of the choices:

# Define the available choices
choices = { "Rock": 0, "Paper": 1, "Scissors": 2 }

# Get the user's choice of the three
user_choice = raw_input("\nType Rock, Paper, or Scissors to play against the computer: ")

# Print what the user's choice, so she knows what she picked
print user_choice

If it worked, when you run your program, it should prompt you to type in Rock, Paper, or Scissors and then display whatever you typed in.

Don’t trust user input

That’s a problem right there—you can’t trust user input. You want the user to type in Rock, Paper, or Scissors, but the user could just as easily type in I hate this game (deliberately incorrect input), Scisors (accidental misspelling), or scissors (not a misspelling but not an exact match for Scissors either).

So, let’s validate the input.

# Define the available choices
choices = { "Rock": 0, "Paper": 1, "Scissors": 2 }

# Get the user's choice of the three
user_choice = raw_input("\nType Rock, Paper, or Scissors to play against the computer: ")

# Make sure the user input is valid
if user_choice in choices:
   print "You chose %s." % user_choice
else:
   print "Your choice of %s is not a valid choice." % user_choice

A few notes about syntax here, because we’re moving a little out of the problem-solving aspect of programming and more into the nuts and bolts of the actual code:

  • Note that the difference between the comments (human-readable notes to yourself) and the actual code the computer runs is just the inclusion (comment) or omission (actual code) of a pound sign # (number sign or hashtag, if that’s what you want to call it).
  • user_choice is a variable, which means you can change its value or you can keep using the same value it has. Think of it sort of like a shopping cart that can hold only one item. So you go to the store, put in a can of soda. Your one-item shopping cart will keep holding that can of soda, until you take the can of soda out and then replace it with a jar of peanut butter. When you get the raw_input from the user, you’re putting whatever the user typed in into the one-item “shopping cart” called user_choice, and every time you reference user_choice, you’ll be referencing whatever the user typed in.
  • choices is a dictionary, which is exactly what it sounds like. In a normal dictionary, you look up a word (rock), and it gives you a definition (a large mass of stone forming a hill, cliff, promontory, or the like). Or in a language-to-language dictionary, you look up a word in one language (rock), and it gives you the corresponding word in another language (roccia). In this case, though, instead of giving you a definition or a foreign language equivalent, the dictionary gives a random number we assign it. (Note for Python veterans: Yes, you can easily use a list instead of a dictionary for this; we’ll go over that later.)
  • Notice how there are indents after the if line and the else line? In some programming languages, there are semi-colons or brackets that separate out parts of the if this is true, do that code. In Python, though, everything is done with spacing. Even though they look like tabs, they’re actually spaces. In this case, I’m using sets of three spaces, but you can pick four spaces or even five, as long as you’re consistent—so one indent would be three spaces, two indents would be six spaces, three indents would be nine spaces, etc.
  • Notice the %s in what you’re printing? That’s just a placeholder to say “I’m going to substitute a string of characters [as opposed to numbers] in this part of the sentence, and I’ll tell you what to substitute in later.” After the lone %, the variable user_choice is what gets substituted in.

Even though we’re “validating” the input, the two problems I mentioned before still apply—a misspelling or lowercase version of the word will appear as invalid input. For now, for simplicity’s sake, let’s let the misspelling piece go. The lowercase issue is a lot easier to fix.

# Define the available choices
choices = { "Rock": 0, "Paper": 1, "Scissors": 2 }

# Get the user's choice of the three
user_choice = raw_input("\nType Rock, Paper, or Scissors to play against the computer: ")

# Make sure the user input is valid
if user_choice.title() in choices:
   print "You chose %s." % user_choice
else:
   print "Your choice of %s is not a valid choice." % user_choice

The addition of .title() to the user_choice variable changes (temporarily, just for that one line) the variable’s value to title case, so ROCK, rock, or even rOCK will all become (just for that one line) Rock.

Picking a choice at random

If we go back to our original comments, it looks as if we need to get the computer choice, now that we have the user choice:

# The user (person) "plays" rock, paper, or scissors

# The computer (program) "plays" rock, paper, or scissors

# We have to determine whether the user won or if there was a tie

# We have to say whether the user won or lost

We could certainly hard-code a computer choice, but we don’t want the computer choosing rock every time. We want the computer to make a random choice every time so it won’t be predictable.

import random

# Define the available choices
choices = { "Rock": 0, "Paper": 1, "Scissors": 2 }

# Get the user's choice of the three
user_choice = raw_input("\nType Rock, Paper, or Scissors to play against the computer: ")

# Make sure the user input is valid
if user_choice.title() in choices:
   print "You chose %s." % user_choice

   # Get the computer's choice by random
   computer_choice = random.choice(choices.keys()) 

   # Say what the computer chose
   print "The computer chose %s as its choice." % computer_choice
else:
   print "Your choice of %s is not a valid choice." % user_choice

A few more syntax notes here:

  • In order to allow for random choices, we have to import the random module.
  • We’re creating a new variable here called computer_choice, but instead of getting the value for it from the user, we’re generating the value randomly. Let’s look at the inside of the parentheses first. Remember our dictionary called choices? That has a lookup value and then a translation. In Python land, the lookup value is called a key and its translation is called the corresponding value. So choices.keys() just means get keys from the dictionary of choices. Then random.choice() just picks a random choice out of those keys. The equal sign assigns that to the variable computer_choice.

Comparing values with if/elif/else

At this point, we have user input and computer input, and we’ve checked that the user input is valid. To actually “play” the game, we don’t want to just say what the user played and what the computer played, but we want to evaluate who won, so we’re going to use our logic from earlier:

import random

# Define the available choices
choices = { "Rock": 0, "Paper": 1, "Scissors": 2 }

# Get the user's choice of the three
user_choice = raw_input("\nType Rock, Paper, or Scissors to play against the computer: ")

# Make sure the user input is valid
if user_choice.title() in choices:
   print "You chose %s." % user_choice

   # Get the computer's choice by random
   computer_choice = random.choice(choices.keys()) 

   # Say what the computer chose
   print "The computer chose %s as its choice." % computer_choice

   # Find the numeric equivalent of what the user plays
   # Find the numeric equivalent of what the computer plays
   # Find the difference between the two
   choice_difference=choices[user_choice.title()]-choices[computer_choice]

   # If the difference is 0, it's a tie
   if choice_difference == 0:
      print "It's a tie!"

   # If the difference is -2 or 1, the user wins
   elif choice_difference == -2 or choice_difference == 1:
      print "The user wins!"

   # Any other scenario, the computer wins
   else:
      print "The computer wins!"

else:
   print "Your choice of %s is not a valid choice." % user_choice

Some more notes on the code changes here:

  • Remember the indents with spaces? Up until this point, we’d used only one set of indents, but now we have if/then statements inside of other if/then statements, so the indents are doubled up sometimes.
  • Note that elif means else if, which means if the first thing isn’t true but this second thing is true, do something (say the user won). And then the else after that just means if neither of the previous conditions is true, do another thing (say the computer won).

Play the game a couple of times and just make sure it’s working.

Extra Credit

In our comments, we find the numeric equivalent of what the user plays (e.g., 0 for rock) and then we separately find the numeric equivalent of what the computer plays (e.g., 2 for scissors), and then we have a separate third step to find the difference between the two.

Instead of doing that, which you can do by assigning those as variables, we can just combine all three steps into one (which is what is above). Instead of “numeric choice is 0 and other numeric choice is 2 and subtract second numeric choice from first one,” it’s just doing it in one step, which is “find the difference between this numeric choice and that one.”

One approach isn’t necessarily better than the other. It depends on what you may want to do later. For example, if you anticipate possibly needing the numeric equivalents again, it would be silly to look it up again when you could just have a variable stand in place for it.

If you have time, try assigning choices[user_choice.title()] and choices[computer_choice] as variables and then using those variables to get choice_difference.

What if we want to keep playing and not have it start over again all the time?

This is where a while loop might come in handy. A loop is just repetition until a certain point. For example, let’s say you have a laundry hamper full of clean and folded clothes, and you have to put those clothes in dresser drawers. The instructions you would give yourself would be “Keep taking clothes out of the hamper and putting them in dresser drawers until the hamper is empty.”

Or imagine you’re on a sports team, and your coach tells you to keep running back and forth on the tennis courts until she tells you to stop. That’s also a loop (“While I’m waiting for my coach to tell me to stop, I’ll keep running back and forth”).

So in this case, we probably want to keep playing Rock, Paper, Scissors until the user tells us she wants to quit.

In the case of the laundry hamper, the “stop” is a natural one (when the hamper is empty). We won’t try this right now, but in the future, you’ll come to know this as a for loop (for each item in the hamper, do put-item-away-in-dresser).

In the case of the sports team, the “stop” is an arbitrary one of your coach telling you to stop when she feels like it. So she may say “stop” in five minutes or in ten minutes or in an hour. This approach is really better suited for a while loop (while the coach hasn’t said stop, keep running back and forth).

The same principle applies to our Rock, Paper, Scissors game. The user may want to play just once or play thirteen times or play a hundred times. We’ll just create a while loop and say essentially “while the user hasn’t said ‘quit,’ let’s have the user keep playing the game.”

So there are three major pieces to this that we need to break down:

  • We need to define a “quit” word for the user to use when she wants to quit the game.
  • We need to define which part of the code is the “game” that we’ll keep playing over and over again.
  • We need to create a while loop that keeps playing the game until the quit word appears.
import random

# Define the quit word
quit_word="quit"

# Define the available choices
choices = { "Rock": 0, "Paper": 1, "Scissors": 2 }
 
def game():
   # Give instructions
   print "\nThis game will keep playing until you type quit"

   # Get the user's choice of the three
   user_choice = raw_input("\nType Rock, Paper, or Scissors to play against the computer: ")
 
   # Make sure the user input is valid
   if user_choice.title() in choices:
      print "You chose %s." % user_choice
 
      # Get the computer's choice by random
      computer_choice = random.choice(choices.keys()) 
 
      # Say what the computer chose
      print "The computer chose %s as its choice." % computer_choice
 
      # Find the numeric equivalent of what the user plays
      # Find the numeric equivalent of what the computer plays
      # Find the difference between the two
      choice_difference=choices[user_choice.title()]-choices[computer_choice]
 
      # If the difference is 0, it's a tie
      if choice_difference == 0:
         print "It's a tie!"
 
      # If the difference is -2 or 1, the user wins
      elif choice_difference == -2 or choice_difference == 1:
         print "The user wins!"
 
      # Any other scenario, the computer wins
      else:
         print "The computer wins!"
 
   else:
      print "Your choice of %s is not a valid choice." % user_choice

while user_choice!=quit_word:
   game()

Some notes:

  • def game() is just defining (as what’s called a function) which part of the code is the game that you want to have the user keep playing over and over again. Note that you then have to indent all the code within (if you’re using TextWrangler, you can highlight all the code and then hit Cmd]).
  • The while loop at the bottom just says “while the user hasn’t typed in the quit word, keep playing the game.”

Debugging (finding/fixing) Errors

When you run this code, you should get the following error:

while user_choice!=quit_word:
NameError: name 'user_choice' is not defined

That’s because the user_choice in the while loop is a global variable (can be used anywhere in the code) and the user_choice in the game() function is a local variable (can be used only in the game() function). In order to have anything outside of the game() function recognize the user_choice variable, you have to make that a global variable:

def game():
   # Make user choice a global variable
   global user_choice

   # Get the user's choice of the three
   user_choice = raw_input("\nType Rock, Paper, or Scissors to play against the computer: ")

But then you’ll run into yet another error, which is

NameError: global name 'user_choice' is not defined

That’s because the while loop test condition (is the user_choice not equal to the quit_word?) comes before the user has even had a chance to put in any input. So we’ll have to give the user_choice a value before the user types in a value:

# Initialize user_choice variable
user_choice=''

while user_choice!=quit_word:
   game()

Now your game should work, but you’ll run into this issue: once you type in quit, instead of quitting, it will say your choice is an invalid choice (because it’s not Rock, Paper, or Scissors):

Type Rock, Paper, or Scissors to play against the computer: quit
Your choice of quit is not a valid choice.

Why is that? Well, let’s look at this while loop step by step:

  1. Initially, you define user_choice as blank (two apostrophes with nothing in between).
  2. The while loop says “Hey, is the user choice not the quit word? It’s not the quit word, so let’s play the game.”
  3. You play a legitimate word like Rock or Scissors.
  4. The while loop says “Hey, this is legitimate. Still not the quit word, so let’s keep playing.”
  5. Then you type quit to quit.
  6. You’re still in the game. Part of the game says “If it’s not a valid choice, say it’s not a valid choice,” so it will tell you that.
  7. Only after the game is finished, does the while loop see your quit word and then say “Okay. Let’s stop.”

So the game itself has to also recognize the quit word. You can do this by changing the last else in the game to be an else if:

      # Any other scenario, the computer wins
      else:
         print "The computer wins!"

   elif user_choice!=quit_word:
      print "Your choice of %s is not a valid choice." % user_choice

# Initialize user_choice variable
user_choice=''

Run it again after making that change and see the difference.

Super extra credit

So we accounted for upper and lower cases (rock, ROCK, rOCK) but not for misspellings or other variations. What if we want to account for those?

We could expand our dictionary to include a bunch of other possibilities, or we could create a separate dictionary that translates misspellings and abbreviations to the proper value. I prefer the latter approach, because it feels cleaner to me, even though there are more steps:

# Define the available choices
choices = { "Rock" : 0, "Paper": 1, "Scissors": 2}

# Make a dictionary of misspellings or alternatives
other_choices = { "Rocks" : "Rock", "Papper" : "Paper", "Scisors" : "Scissors", "Scissor": "Scissors", "Scisor": "Scissors", "R" : "Rock", "P": "Paper", "S": "Scissors" }

So what this accounts for is people misspelling scissors or just putting the first letter instead of the full word. Makes the game a little more flexible.

One more thing to do, which is the translation of the misspelling or abbreviation to the actual original dictionary key:

      # Double-check user input is one of the keys
      if user_choice.title() in other_choices:
         user_choice=other_choices[user_choice.title()]

      if user_choice.title() in choices:
         print "Your choice of %s is a valid choice." % user_choice

We’re basically assigning a new user_choice based on how the original user_choice went, and then using that new user_choice the way we did before with the original dictionary of choices.

Still other approaches

Super credit to Chelsea for helping think about this problem in still other ways (yes, there is always more than one “right” answer!). Instead of a dictionary, you can use a list:

choices = [ "Rock", "Paper", "Scissors" ]

select a list item at random instead of a key:

computer_choice = random.choice(choices)

and then use the list index as the number to compare:

choice_difference=choices.index(user_choice.title())-choices.index(computer_choice)

For beginners, even though it’s a bit counterintuitive, I would recommend the dictionary over the list, so they can easily see the matching up of the number to the Rock, Paper, or Scissors. Generally speaking, though, you want to use a list whenever possible instead of a dictionary.

Another cool idea Chelsea suggested was storing a dictionary of which play beats the other play:

choices = { "Rock": "Paper", "Paper": "Scissors", "Scissors": "Rock" }

It may seem a bit redundant, but it offers flexibility in case the rules ever change.

Whoa! That was a lot!

Yes, none of this “Hello, World” business you usually get, but I really have tried to take you step by step on how to build a program and use a lot of the standard building blocks (modules, functions, variables, dictionaries, while loops, conditions).

If you have dabbled in programming before, a lot of this should seem familiar to you, and this may have been a good refresher exercise for you.

If you have never programmed before, you may not have gotten all of this, but hopefully you got something. I’d highly encourage you to repeat the exercise from scratch and make sure you are retyping code and not just copying and pasting (it makes a difference).