Bonus Material

CS 106

Project Notes

  • Start simple.
    • Get a blank screen first.
    • Don’t build out all of the details yet.
  • Work smart, not hard. If you’re spending an hour just typing stuff into Thonny, you’re doing it wrong.
  • Make connections with your major, life story, friends, …
  • Make it your own. “What if?”, “but when my family plays the game, we ___”

You don’t need to use any particular framework or library from this class. But do it if it saves time.

Comprehensions

A compact replacement for some accumulator patterns.

List Comprehensions

nums = list(range(0, 10))
nums
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Simple list comprehensions

squares_acc = []
for num in nums:
    squares_acc.append(
        num * num
    )
squares_comp = [
    num * num
    for num in nums
]
squares_comp
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Including only certain items

squares_acc = []
for num in nums:
    if num % 2 == 1:
        squares_acc.append(
            num * num
        )
squares_comp = [
    num * num
    for num in nums
    if num % 2 == 1
]
squares_comp
[1, 9, 25, 49, 81]

Dict and Set comprehensions

{word: len(word) for word in ['hello', 'world']}
{'hello': 5, 'world': 5}
{char.lower() for char in 'Hello, world!' if char != ' '}
{'!', ',', 'd', 'e', 'h', 'l', 'o', 'r', 'w'}

Comprehensions Summary

  • “declarative” style: say what you want, not how to make it
    • But no magic; it’s just shorthand for the for loop.
  • A tool in the toolbox
    • especially useful if you want to construct a list to pass on to something else
    • but don’t overuse it.

Comprehensions Exercises

  1. Write the non-comprehension version of the following comprehension:
squares_comp = [num * num for num in nums if num % 2 == 1]
squares_comp
[1, 9, 25, 49, 81]
  1. Write a dict comprehension that gives the indices of each word in a list of words. For example, if words = ['hello', 'world'], the output should be {'hello': 0, 'world': 1}.

  2. Do these:

{word: len(word) for word in ['hello', 'world']}
{char.lower() for char in 'Hello, world!' if char != ' '}
{'!', ',', 'd', 'e', 'h', 'l', 'o', 'r', 'w'}

Some algorithms

Searching algorithms

Suppose we have a sorted list:

breakpoints = [60, 63, 67, 70, 73, 77, 80, 83, 87, 90, 93]
grades =      'F|D-|D|D+|C-|C|C+|B-|B|B+|A-|A'.split('|')
assert len(breakpoints) == len(grades) - 1

We want to find the letter grade for, say, 89. Can we do this faster than searching the whole list?

Exercise: Write the code to search the whole list.

def grade_naive(score):
    grade = grades[-1]
    for i in range(len(breakpoints)):
        if score < breakpoints[i]:
            grade = grades[i]
            break
    return grade
[grade_naive(score) for score in [33, 99, 77, 70, 89, 90, 100]]
['F', 'A', 'C+', 'C-', 'B+', 'A-', 'A']

Binary Search (Bisection) Algorithm

Trick: check the item in the middle element to see whether to look in the left or right half.

from bisect import bisect
def grade_bisect(score):
    """Return the grade corresponding to a score."""
    i = bisect(breakpoints, score)
    return grades[i]

[grade_bisect(score) for score in [33, 99, 77, 70, 89, 90, 100]]
['F', 'A', 'C+', 'C-', 'B+', 'A-', 'A']

Timing comparison

%timeit [grade_naive(score) for score in [33, 99, 77, 70, 89, 90, 100]]
1.66 μs ± 7.1 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
%timeit [grade_bisect(score) for score in [33, 99, 77, 70, 89, 90, 100]]
The slowest run took 5.23 times longer than the fastest. This could mean that an intermediate result is being cached.
1.26 μs ± 806 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

Sorting

import random
words = random.__doc__.split()
'|'.join(words[:20])
'Random|variable|generators.|bytes|-----|uniform|bytes|(values|between|0|and|255)|integers|--------|uniform|within|range|sequences|---------|pick'
'|'.join(sorted(words, key=len)[-10:])
'2**19937-1.|extensively|implemented|threadsafe.|distributions|distributions|distributions|----------------------|------------------------------|---------------------------------------------'
def last_letter(word):
    return word[-1]
'|'.join(sorted(words, key=last_letter)[-10:])
'notes|is|is|generators|is|executes|element|It|most|extensively'

Simulation Stuff

Some material useful for simulations

Serialization (save and load data)

CSV files

  • Pro: tabular data to share with others
  • Cons: can’t store nested data; reader has to guess data types (e.g., Bar Harbor, Maine has ZIP code 4609?)
  • sometimes better to use Excel (XLSX)

JSON (JavaScript Object Notation) files

  • Pros: Used a lot for APIs because…
    • Can store nested objects (e.g., board game states)
    • Looks like normal Python (actually, JavaScript) code
    • Fast; lots of programming languages can work with it
  • Con:
    • few applications can work with them

Pickle

Making random decisions

Suppose you want to add thunderstorms in your population simulation. For simpliciticy, they happen each day with probability p. Remember that random.random() generates a random number between 0 and 1. What fills in the blank?

if ____:
    print("Thunderstorm!")
if random.random() < p:
    print("Thunderstorm!")

What if you have 3 options and you want to choose equally between them?

r = random()
if ___:
    print("A")
elif __:
    print("B")
else:
    print("C")
r = random()
if r < 1/3:
    print("A")
elif r < 2/3:
    print("B")
else:
    print("C")

Cool tools

Course Evals

Do them now. Come get the prof out in the hall when everyone’s done.

Some standard library features

Code you don’t have to write, or even install!

Counting stuff

from collections import Counter
Counter('How many times does each letter appear?')
Counter({' ': 6,
         'e': 6,
         'a': 4,
         't': 3,
         'o': 2,
         'm': 2,
         's': 2,
         'r': 2,
         'p': 2,
         'H': 1,
         'w': 1,
         'n': 1,
         'y': 1,
         'i': 1,
         'd': 1,
         'c': 1,
         'h': 1,
         'l': 1,
         '?': 1})

Regular expressions

import re
ssn_regex = re.compile(r"""
      ^  # match the beginning of the string
      (\d{3}) # match exactly 3 digits
      -       # match one dash
      (\d{2}) # match exactly 2 digits
      -
      (\d{4}) # match exactly 4 digits
      $       # match end of string
    """, re.VERBOSE)

def is_valid_ssn(ssn):
    match = ssn_regex.match(ssn)
    if match:
        print(match.groups())
        return True
    return False
is_valid_ssn("123-45-6789")
('123', '45', '6789')
True
is_valid_ssn("51-345-5212")
False

enumerate

letters = list("ABCDEFG")
for i in range(len(letters)):
    print(i, letters[i])
0 A
1 B
2 C
3 D
4 E
5 F
6 G
letters = list("ABCDEFG")
for i, letter in enumerate(letters):
    print(i, letter)
0 A
1 B
2 C
3 D
4 E
5 F
6 G

Code Structure

Data vs Code

Example: Spelling Alphabet

def spell(letter):
    if letter == "A":
        return "Alfa"
    elif letter == "B":
        return "Bravo"
    elif letter == "C":
        return "Charlie"
    elif letter == "D":
        return "Delta"
    elif letter == "S":
        return "Sierra"
    else:
        return "?"
[spell(letter) for letter in "CS"]
['Charlie', 'Sierra']

Is there a better way?

code_word = {
    "A": "Alfa",
    "B": "Bravo",
    "C": "Charlie",
    "D": "Delta",
    "S": "Sierra",
}
def spell(letter):
    if letter in code_word.keys():
        return code_word[letter]
    return "?"
[spell(letter) for letter in "CS"]
['Charlie', 'Sierra']
[code_word.get(letter, "?") for letter in "CS"]
['Charlie', 'Sierra']

Aside: Multiple Cursors

Live demo. CodeMirror

LLMs

Using large language models

import llm
model = llm.get_model("gemini/gemini-2.5-flash")
response = model.prompt("Replace some words to make the following sentence more absurd: 'The quick brown fox jumps over the lazy dog.'")
print(response.text())
Here are a few options, playing with different types of absurdity:

1.  **The disgruntled cloud contemplates beneath the philosophizing pebble.**
2.  **The fluffy purple toaster hiccups through the apathetic washing machine.**
3.  **The iridescent narwhal procrastinates into the existential sloth.**
4.  **The silent magenta sock breathes upon the enthusiastic potato.**
5.  **The whistling polka-dotted carrot sings to the melancholy stapler.**

To set this up:

  1. Install the llm package (Manage Packages in Thonny, or pip install llm).
  2. Install llm-gemini in the same way.
  3. Get a Gemini API key from https://aistudio.google.com/app/apikey
  4. Save the API key: go in Thonny Settings, General tab, and put in the Environment Variables:
LLM_GEMINI_KEY=

and paste the key in the right-hand side (without quotes). Then restart Thonny.

(alternatively, run llm keys set gemini on a system Terminal.)

Reference: llm API

Conversation Mode

This way the model can remember the context of the conversation.

import llm
model = llm.get_model("gemini/gemini-2.5-flash")
conversation = model.conversation()
response = conversation.prompt("Five fun facts about the moon, one phrase each.")
print(response.text())
1.  Causes Earth's ocean tides.
2.  Humans walked upon its surface.
3.  Always shows its same face.
4.  Covered in countless craters.
5.  It's slowly drifting away.
response = conversation.prompt("Now the sun.")
print(response.text())
1.  Its core fuses hydrogen.
2.  It is a yellow dwarf star.
3.  Holds all planets in orbit.
4.  Contains over 99% of system's mass.
5.  Light takes 8 minutes to reach Earth.

LLMs can call Python functions

def uppercase(text: str) -> str:
    print("LLM called the uppercase function with:", text)
    return text.upper()
conversation = model.conversation(tools=[uppercase])
response = conversation.chain("Always use functions. Repeat everything you've heard so far.")
print(response.text())
LLM called the uppercase function with: Always use functions. Repeat everything you've heard so far.
ALWAYS USE FUNCTIONS. REPEAT EVERYTHING YOU'VE HEARD SO FAR.

The : str and -> str are Python syntax to indicate the types of the input and output.