Python Tips and LLMs

CS 108

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 or makes your project better.

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.35 μs ± 90.8 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]]
476 ns ± 29.6 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")

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 **sparkly toaster calculates** through the **existential sock puppet.**
2.  The **whispering nebula manifests** within the **polka-dotted concept of Tuesday.**
3.  The **fluffy purple rhinoceros spontaneously combusts** beside the **bewildered garden gnome.**
4.  The **sarcastic refrigerator tap-dances** on the **forgotten echo of a sneeze.**
5.  The **sentient teacup argues** with the **philosophical puddle.**

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.  Earth's tidal orchestrator.
2.  Airless, silent vacuum world.
3.  Sole celestial human landing site.
4.  Always shows the same face.
5.  Born from a giant impact.
response = conversation.prompt("Now the sun.")
print(response.text())
1. Our solar system's gravitational anchor.
2. Earth's ultimate energy source.
3. A luminous ball of hydrogen and helium.
4. Rotates faster at its equator.
5. Will someday become a red giant.

System Prompts

conversation = model.conversation()
response = conversation.prompt(
    "A 1-sentence encouragement for students reaching the end of the semester.",
    system="Respond in all lowercase, with some silly emoji."
)
print(response.text())
you're so close to the finish line, crush those final tasks because freedom is waiting! 🎉🐙

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: Repeat everything you've heard so far.
REPEAT EVERYTHING YOU'VE HEARD SO FAR.

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

Working with Paths

pathlib

from pathlib import Path
home = Path.home()

Make a folder (“directory”):

data_dir = home / "cs108-example-data"
data_dir.mkdir(exist_ok=True)

Create some files:

for filename in ["file1.txt", "file2.txt", "file3.txt"]:
    path = data_dir / filename
    path.write_text(f"This is {filename}")

Reading Files

for path in data_dir.iterdir():
    print(path.name, "contents:", path.read_text())
file2.txt contents: This is file2.txt
file3.txt contents: This is file3.txt
file1.txt contents: This is file1.txt

Appendix: OpenAI Client Library

The openai client library is another way to talk to LLMs. It works not just with OpenAI’s own models but with any “OpenAI-compatible” server — including self-hosted models running on vLLM.

Install it with pip install openai (or Manage Packages in Thonny).

Setup

from openai import OpenAI
client = OpenAI(
    base_url="https://vllm.thoughtful-ai.com/v1",
    api_key="not-needed"  # this server doesn't require one, but the library insists
)
MODEL = "Qwen/Qwen3.5-9B"  # https://huggingface.co/Qwen/Qwen3.5-9B

# Qwen3 defaults to "thinking mode" (slow). Turn it off for these examples:
NO_THINKING = {"chat_template_kwargs": {"enable_thinking": False}}

Simple Prompt

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "user", "content": "Replace some words to make the following sentence more absurd: 'The quick brown fox jumps over the lazy dog.'"}
    ],
    extra_body=NO_THINKING,
)
print(response.choices[0].message.content)
Here are a few ways to replace words to make the sentence dramatically more absurd, ranging from silly to nightmarish:

**Option 1: The Culinary Nightmare**
> "The **fermented green** fox **inhales** over the **haunted xyz**."

**Option 2: The Cosmic Absurdity**
> "The **omniscient fluorescent** fox **teleports** over the **quantum toaster**."

**Option 3: The Biological Chaos**
> "The **dizzy jellybean** fox **dissects** over the **angry teapot**."

**Option 4: The Maximum Chaos**
> "The **sluggish radioactive** fox **undoes reality** over the **sleeping volcano**."

Conversation Mode

Unlike the llm library, the OpenAI client is stateless: we keep the conversation history ourselves as a list of messages.

messages = [
    {"role": "user", "content": "Five fun facts about the moon, one phrase each."}
]
response = client.chat.completions.create(
    model=MODEL, messages=messages, extra_body=NO_THINKING)
reply = response.choices[0].message.content
messages.append({"role": "assistant", "content": reply})
print(reply)
1. The moon is bumpy from lava have created over 30,000 "lava tubes" on the lunar surface.
2. A day on the moon lasts about 29.5 Earth days.
3. One cubic inch of lunar soil weighs 129 pounds on Earth.
4. The moon is drifting away from Earth at a speed of 3.8 centimeters per year.
5. The moon reflects light, but it does not generate its own light.
messages.append({"role": "user", "content": "Now the sun."})
response = client.chat.completions.create(
    model=MODEL, messages=messages, extra_body=NO_THINKING)
print(response.choices[0].message.content)
1. The sun is so huge that over a million Earths could fit inside it.
2. A single drop of sunspots contains ten times the number of air molecules in our entire atmosphere.
3. If you could walk fast enough, you could run over to the nearest star (Proxima Centauri) before reaching the edge of our galaxy.
4. The sun's atmosphere is millions of degrees hotter than its visible surface.
5. The sun produces enough energy every second to power every human activity on Earth for billions of years.

System Prompts

The system prompt is just another message in the list, with role "system":

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "Respond in all lowercase, with some silly emoji."},
        {"role": "user", "content": "A 1-sentence encouragement for students reaching the end of the semester."}
    ],
    extra_body=NO_THINKING,
)
print(response.choices[0].message.content)
you absolutely crushed it this semester and your brain deserves a nap pillow right now! 🧠💤🌟

Tools: Defining the Functions

Two basic filesystem tools, using the pathlib code from earlier:

from pathlib import Path

def list_folder(path: str) -> str:
    """List the files and folders inside a folder."""
    items = [p.name for p in Path(path).iterdir()]
    return "\n".join(items)

def read_file(path: str, line_numbers: bool = False) -> str:
    """Read a file's contents, optionally with line numbers."""
    text = Path(path).read_text()
    if line_numbers:
        lines = text.splitlines()
        return "\n".join(f"{i+1}: {line}" for i, line in enumerate(lines))
    return text

Tools: Describing Them to the Model

The OpenAI API wants a JSON description of each tool, with a schema for its arguments.

tools = [
    {
        "type": "function",
        "function": {
            "name": "list_folder",
            "description": "List the files and folders inside a folder.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "Path to the folder"}
                },
                "required": ["path"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Read a file's contents, optionally prefixed with line numbers.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "Path to the file"},
                    "line_numbers": {"type": "boolean", "description": "Include line numbers?"},
                },
                "required": ["path"],
            },
        },
    },
]

The llm library generated this for us automatically from the function’s type hints; here we write it by hand. (Tip: ask an LLM to do it for you!)

Tools: The Conversation Loop

Ask the model, run any tools it requests, feed results back, repeat until it’s done.

import json

messages = [{"role": "user", "content":
    f"What files are in {data_dir}? Summarize what one of them says."}]

while True:
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
        extra_body=NO_THINKING)
    message = response.choices[0].message
    messages.append(message)

    if not message.tool_calls:
        break

    for tool_call in message.tool_calls:
        name = tool_call.function.name
        args = json.loads(tool_call.function.arguments)
        print(f"Model called {name}({args})")
        if name == "list_folder":
            result = list_folder(**args)
        elif name == "read_file":
            result = read_file(**args)
        print(f"Tool result:\n{result}")
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": result,
        })

print(message.content)
Model called list_folder({'path': '/Users/ka37/cs108-example-data'})
Tool result:
file2.txt
file3.txt
file1.txt
The files in `/Users/ka37/cs108-example-data` are:
- file1.txt
- file2.txt
- file3.txt

Here is a summary of **file1.txt**:

It appears to be a simple introductory text file that likely states: "This is file number 1." or serves as a placeholder to demonstrate file handling capabilities in the CS108 course materials.