CS1602Introduction to Computation
Lecture 8Part 3 Problem Solving with AIAI Level 1

AI Programming and the Linux Workspace

Use real chat, code review and VS Code debugging for local AI assistance, then explore Linux history, kernels, distributions and simple independent terminal examples.

By the end of this lecture you should be able to

  • Own the main implementation and use chat for local code assistance and debugging
  • Retain candidates and run independent checks before a concrete follow-up
  • Read diffs, explain the core code and disclose actual assistance
  • Relate Unix, GNU and Linux and distinguish a kernel from a distribution
  • Separate terminals, shells and Python and use simple file and terminal commands
  • Operate the VS Code debugger and check advice against variables, call frames and execution
Contents

This lecture has two parts: AI-assisted programming and debugging, followed by Linux. A file-type counter is the example for the first part. Linux begins with the film and continues with independent hello.py and terminal examples.

The taught sequence comes first. Complete reference implementations, the directory adapter and the existing detailed material follow as optional reading.

Part 1: AI-assisted programming and debugging

Understand the LLM before asking it for help

A large language model (LLM) is a neural network with many learned numerical parameters. For the generative models discussed here, developers do not write a separate answer rule for every question. Training adjusts the parameters using many examples, allowing the model to learn relationships in language, code and context.

Training and use are different processes

  1. Pretraining uses text, code, documentation and other material for prediction practice. The model predicts continuations from context; training compares those predictions with the data and adjusts parameters. Solving many such tasks can develop useful representations of syntax, knowledge associations, APIs and program structure. This does not guarantee a correct answer to every question.
  2. Instruction and feedback training uses request/response examples and feedback to improve how a model follows requests, explains and answers. Recipes differ across models; this is a simplified common picture.
  3. At use time, your question, relevant code, errors and conversation context are supplied to the trained model, which generates a response. Adding code to an ordinary chat usually changes the current input, not the model parameters through immediate retraining.

Stanford CS146S’s introductory slides separate generation, training and practical capability. This lecture follows that distinction without requiring layer counts, vector dimensions or historical parameter estimates.

How a response is generated

A token is a text unit used by a model. It may be a word, part of a word, punctuation or another fragment; the tokenizer determines the boundaries. It is not necessarily one character or one whole word. Given the available context, the model predicts a probability distribution over the next token, selects a continuation, appends it, and repeats until stopping. Selection may be stochastic rather than always choosing the highest-probability token.

For example, The sky is admits several continuations. Once a new fragment is added, the updated context is used for the next prediction. This is a conceptual illustration, not a measured probability table from a model. The process does not consult only the preceding word’s frequency. 3Blue1Brown’s visual explanation illustrates the repeated generation loop.

Code is structured text too. Training involving code and documentation can connect syntax, APIs, descriptions and implementations; instruction training helps turn requests such as “explain this error” or “suggest a local change” into responses. The same model can therefore answer questions, explain code and produce fragments. Generating code or an explanation does not mean Python ran it, or that the model knows your current files, interpreter and entire contract. Execution evidence must be obtained separately.

Model, chat application and coding agent

LayerRole
LLMGenerates outputs from the supplied context
Chat applicationOrganizes requests and responses; users can paste code, errors and relevant material
Coding agentApplication software can execute model-requested file or terminal tools and return their results to the model

A tool call is an action actually executed by software. Merely generating “I tested it” does not create a test result. Berkeley’s agent lecture distinguishes actions and observations; MIT Missing Semester places file and terminal tools in a concrete development workflow.

At Level 1, use chat for local help: explanations, debugging and limited code generation or revision. Edit, integrate, run and verify the code yourself. Do not ask for the complete assigned solution or let an agent operate on the project. Level 2 opens complete implementations and agent-driven editing, execution and fixes. A chat-like interface does not itself determine the workflow.

Four useful kinds of local request

  • Understand code: ask about state changes, then trace them yourself.
  • Produce a fragment: identify the location, scope and constraints of the small change.
  • Diagnose an error: provide the smallest failing input, relevant code, full traceback, runtime and expected behavior; connect the cause to the evidence before patching.
  • Review checks: ask about missing boundaries, while keeping expectations grounded in the contract.

These are ways of cooperating, not mandatory prompt wording. Stanford’s prompting slides compare concrete requests with different context. This lecture uses that comparison rather than a catalogue of prompting terminology.

Classroom task: review a file-type counter

Use the instructor-provided file-type counter to practice understanding code, spotting a mismatch and making a local revision. Establish the rules, inspect candidates/first.py, and keep inventory.py as the complete reference and check_inventory.py as independent checks. This is classroom reading and execution material, not an additional lab submission.

Download the classroom ZIP. It also includes factorial.py and .vscode/launch.json for the debugger demonstration.

Decide the behavior and provide relevant context

Level 1 permits local code generation or revision, explanations and debugging help; it does not permit asking AI for the complete solution to an assigned problem. Use chat to obtain suggestions or a limited fragment, then review, edit, run and verify it yourself. Calling a complete solution “one function” does not change this boundary.

Level 2 permits complete implementations and coding agents that create or edit project files and run or fix code automatically. The distinction is the actual workflow: a coding agent with a chat window is still an agent. Both levels require disclosure and verification.

The interface is count_extensions(names: list[str]) -> dict[str, int]. Each input element is a nonempty basename, not a path; the list itself may be empty. The contract is:

  • Count only the final suffix, normalized to lower case.
  • Put names without a dot, lone leading-dot names such as .env, and trailing-dot names such as notes. under "(none)".
  • Count archive.tar.gz under gz and .config.json under json. This classifies names, not the actual contents of a file.
  • Count every occurrence, preserve the input list and return a fresh dictionary on every call.

This is an instructor-authored example request, not a quotation from a student’s conversation:

Here is the local code I am inspecting:
ext = parts[1]
counts[ext] = counts.get(ext, 0) + 1
L01.PDF and L02.pdf should share the pdf key, but currently use two keys.
Explain why and suggest only a change to ext.
Preserve the input and loop. Do not supply the complete function.
I will integrate, run and check the change myself.

Paste the relevant snippet, contract and observed result rather than your whole computer. Ask for an explanation or a familiar equivalent when unfamiliar syntax appears. Solo assignment 1 remains Level 0 and must not be handed to an AI for generation or rewriting.

Four real chat cases: context, reply and your own checks

The Chinese requests and quoted replies below are from the actual classroom DeepSeek Chat conversations. Code and constraints are retained. A model reply and an independently executed check are different evidence. Preparing an erroneous snippet for diagnosis does not mean DeepSeek wrote that error.

1. Understand the default argument to get

Actual request, including code:

这段代码我有点没看懂,get 后面的 0 是干嘛的?能拿第一次循环简单说说吗?

counts = {}
for ext in ["pdf", "pdf", "txt"]:
    counts[ext] = counts.get(ext, 0) + 1

Reply excerpt:

如果字典里没有 ext 这个键,就返回 0;如果有,就返回它对应的值。

In the first iteration, get returns 0 for the absent key pdf. After adding one and assigning it, counts is {'pdf': 1}. Run the following instrumented version yourself; it does not claim the chatbot ran it:

counts = {}
for ext in ["pdf", "pdf", "txt"]:
    counts[ext] = counts.get(ext, 0) + 1
    print(ext, counts)

Actual conversation screenshot.

2. Supply adjacent code before accepting a local change

The initial request described the grouping issue:

我发现 PDF 和 pdf 被分开统计了。我想把它们算在一起,是不是只要改 ext = parts[1] 这一行?其他地方先不动。

It omitted adjacent code. The reply proposed lower(), and also mentioned strip(). The follow-up supplied the code, its use as a dictionary key and the whitespace constraint:

我后面就是用 ext 当字典的键,没有别的判断,文件名里的空格也要保留。代码是这两行:

ext = parts[1]
counts[ext] = counts.get(ext, 0) + 1

那我只加 lower() 可以吗?

Reply excerpts:

这样 PDF 和 pdf 会合并成同一个键 pdf,同时不会动到文件名中的空格。 不要加 .strip(),因为你明确要保留空格。

The accepted local change is:

ext = parts[1].lower()

lower() creates a new suffix string. Counting uses that local key and leaves the original filename list unchanged. Check the merged PDF/pdf result and rerun existing no-suffix cases. Whether strip() belongs here depends on the contract, not on the fact that an assistant proposed it.

Actual conversation screenshot.

Check side effects: two names, one list

This example shows why we also check whether the input was changed. Assignment does not copy the list, so both prints see the changed first filename. A local suffix normalization need not mutate that list.

def prepare(names):
    result = names
    result[0] = result[0].lower()
    return result

files = ["L01.PDF", "L02.pdf"]
print(prepare(files))
print(files)

3. Send code, input, expected behavior and the complete traceback

The actual request explained that README has no suffix, should return (none), and asked for a local revision. It included this code:

def suffix(name):
    parts = name.split(".")
    return parts[1].lower()

print(suffix("README"))

The complete error sent with it:

Traceback (most recent call last):
  File "debug_suffix.py", line 5, in <module>
    print(suffix("README"))
          ~~~~~~^^^^^^^^^^
  File "debug_suffix.py", line 3, in suffix
    return parts[1].lower()
           ~~~~~^^^
IndexError: list index out of range

Reply excerpt:

问题出在 "README".split(".") 返回的是 ["README"](只有一个元素),所以 parts[1] 越界了。

The next function is DeepSeek’s actual first proposal, followed by three inputs that we used to check it ourselves:

def suffix(name):
    parts = name.rsplit(".", 1)
    if len(parts) == 1:
        return "(none)"
    return parts[1].lower()

for name in ["README", "File.TXT", "notes."]:
    print(name, repr(suffix(name)))

Actual output:

README '(none)'
File.TXT 'txt'
notes. ''

README no longer raises an exception, but notes. returns an empty string. The existing contract requires (none), so the proposal is incomplete. A correct diagnosis of the exception does not establish correctness on all inputs.

Actual conversation screenshot.

4. Ask for test ideas without claiming implementation review

This actual request supplied requirements and the cases already tried, but no implementation:

我写了个统计文件后缀的小函数,PDF 和 pdf 要算一类,没有后缀的算 "(none)",也不能改原来的文件名。我目前只试过 ["a.PDF", "b.pdf"],结果是 {"pdf": 2}。这就算测全了吗?还应该先试哪两种情况呀?

Reply excerpt:

所以“先试哪两种”:无后缀 和 隐藏文件/多点文件名。

This supplies test directions, not proof that a particular function is correct. We obtained the following results by running the complete reference ourselves. Requirements still determine the expected behavior of suffix-free, hidden and multi-dot names:

README {'(none)': 1}
archive.tar.gz {'gz': 1}
.bashrc {'(none)': 1}

Actual conversation screenshot.

Observe runtime state in VS Code, then follow up in chat

If final output is insufficient, pause the program and inspect variables and call frames. A chatbot has not observed that paused state unless you provide the code, input, paused line, selected frame and values. Reuse factorial(3) from L7 here; this is not an additional lab task.

  1. Install Microsoft’s Python and Python Debugger extensions. Unzip the classroom ZIP and open its complete folder through File → Open Folder.
  2. Open factorial.py. Use View → Command Palette → Python: Select Interpreter and select the course Python 3.13.
  3. Click beside line 4 to set a red breakpoint. Execution pauses before the line executes.
  4. In Run and Debug, select the supplied Classroom factorial(3) configuration. Use F5 / Run → Start Debugging. Ordinary Run Python File is not breakpoint debugging.
  5. The first stop has n=3. Continue twice to reach n=2 and n=1. Inspect Variables. Selecting the outer factorial(3) frame in Call Stack shows its own n=3, without re-executing it.
ActionCommon keyWhat it does
Step IntoF11Enter the call on the current line
Step OverF10Execute the line without normally stepping through its calls
Step OutShift+F11Run until this call returns
ContinueF5Run to a breakpoint or completion

An internal breakpoint can still interrupt a step. Function keys may require Fn; toolbar buttons and the Run menu are alternatives. See the official VS Code guide. The actual classroom GUI capture used local Python 3.14.7; the same paused line and n=1/2/3 frame states were independently checked with course Python 3.13.

A follow-up can include the same factorial.py source, line 4, the current factorial(1) frame’s n=1, and the selected outer factorial(3) frame’s n=3, then ask why the values differ. Supply both code and state. This is a suggested follow-up format, not another claimed real conversation.

Ruff adds static checks, a debugger reveals a particular execution, and independent tests check selected behavior. Part 1 ends with context → local assistance → your own editing/debugging → execution and checks.

Execution also requires CPU time, memory and file services. Part 2 uses the film to introduce the operating system, then practices independent terminal examples.

Part 2: understand and use Linux

From Unix and GNU to Linux

Unix began at Bell Labs in 1969. This authentic archive photograph shows Thompson and Ritchie with a PDP-11 in the early 1970s; the photograph’s date is not the date of Unix’s creation.

Ken Thompson seated and Dennis Ritchie standing at a PDP-11

Image and historical account: Bell Labs archive. Useful tasks, including text processing, helped the system develop. Terminals connected people to a shared machine.

The GNU project was announced in 1983 with the aim of building a free software system and tools. In 1991, University of Helsinki student Linus Torvalds began the Linux kernel project and announced it publicly in August. Linux is an independently written Unix-like kernel, not renamed Unix source code. GNU history, Linux Foundation account.

Linus Torvalds, photographed in 2004

Photo: kuvaaja / Linuxmag.com, 2004, original and CC BY-SA 3.0 license, uncropped. This is not a photograph of the 1991 announcement.

The original Linux 0.01 release notes, preserved by kernel.org, describe limited early hardware support and the need for a shell, compiler, libraries and other software. A kernel and a complete usable system have different scopes. Open source permits reading, modification and collaboration under licenses; it does not remove license conditions.

What does the kernel manage?

A browser, music player and Python program can run together. The kernel schedules processes, manages memory and address spaces, and provides file, network and device access through components such as filesystems and drivers. The Python interpreter handles Python syntax and dictionary operations. File reads, process creation and terminal output require system services. Linux kernel documentation.

For this particular terminal invocation:

Terminal displays interaction
    ↓
Bash interprets a command and starts Python
    ↓
Python executes hello.py
    ↓ when file, process or output services are needed
System calls → kernel → filesystems, drivers and hardware

GUI applications may launch programs directly; a shell is not mandatory for every program. Multiple cores can run work in parallel, while one core can also schedule different tasks. An application does not permanently own one CPU core.

Kernel, distribution, desktop, terminal and shell

ComponentResponsibility in this example
Linux kernelProcess, memory, device and file services
A distribution such as UbuntuPackages the kernel with tools, libraries, package management and applications
Desktop environmentGraphical windows, launchers and file management
Terminal programDisplays text input/output and hosts terminal sessions
A shell such as BashInterprets commands, redirections and pipelines, and launches programs
Python interpreterExecutes Python source

Ubuntu is a Linux distribution; the Linux kernel is one of its components. A distribution combines, configures and maintains the kernel with tools such as Bash, libraries, software management such as apt, and applications. People often use “Linux” broadly for such systems; the installed distribution might be Ubuntu, Debian GNU/Linux or Fedora Linux. Desktop and application choices depend on the distribution, edition and installation. Ubuntu project.

Linux systems can have graphical desktops; servers can omit them. Systems using the Linux kernel need not share the same GNU tools or userspace. One desktop screenshot cannot represent every Linux system.

Practical differences from Windows and macOS

SituationCommon Windows environmentCommon Linux environment
PathsC:\Users\Ana\project/home/ana/project, with filesystems mounted under a single / root
Filename caseCommonly insensitive by default; configurable per directoryCommon home filesystems are sensitive; mounted filesystems may differ
ShellPowerShell, cmd and other choicesBash and other choices
Software toolsGUI installers, stores and package managersDistribution package managers and other installation methods

Both systems support permissions, graphical interaction and command-line tools. Neither “Windows has no terminal” nor “Linux only has a text screen” is a useful distinction.

WSL 2 runs a real Linux kernel in a lightweight VM on Windows. A Windows C: drive is typically mounted at /mnt/c in that environment. Check the actual path, file location and interpreter. WSL architecture, filesystems and case behavior.

macOS uses the XNU kernel. Many Unix-style commands look similar, but XNU is not the Linux kernel. BSD tools on macOS and GNU tools commonly used on Linux can have different options: consult the current environment’s help. Apple XNU.

Practice with a few independent terminal examples

Create a directory in your own practice location. Use an editor to save hello.py containing print("hello"):

mkdir -p terminal-demo
cd terminal-demo
touch notes.txt
# Save hello.py in the editor before running it
python3 hello.py

The program prints hello. touch creates an empty file or updates timestamps; it does not write code or empty existing files. Use pwd to locate yourself, quote paths containing spaces, use Tab for completion and check python3 --version.

Add a small helper.py file, then find and count the two Python files:

find . -type f -name '*.py' | wc -l

This example produces 2. find selects .py regular files under the current directory. The pipe passes path text to wc -l, which counts newline characters. Filenames here contain no newline, so each path occupies one line. The presentation lines of ls -l are not an exact file count.

Save program output and find a line of source:

python3 hello.py > output.txt
cat output.txt
grep -n -F 'print(' hello.py

cat prints hello; grep prints 1:print("hello"). -n adds line numbers; -F matches literal text. > overwrites, >> appends, and a plain > does not capture stderr. See the find manual and grep manual.

Ctrl+C interrupts a foreground command. Up and Ctrl+R recall history. Consult man, --help or the shell’s help; q commonly exits a pager.

Linux recap

  • The kernel manages CPU, memory and devices.
  • A distribution combines the kernel, tools, libraries and applications.
  • A shell interprets commands and starts programs.

The taught sequence ends here: explain the system layers, locate a directory, run Python, search files or text, save output and find help.

What follows is optional reference material beyond the taught sequence.

Optional reading: complete references and the directory adapter

The following material supports independent reading and reproduction. It is not a project-integration task for the Linux half of the lecture.

Complete reference implementation

The following is instructor-provided reading material, included in full for independent execution and explanation. Showing complete textbook code does not permit students to ask AI for a complete assigned solution at Level 1. Its actual authoring history is unchanged: this course-authoring session generated and ran the function; the earlier version used ext = parts[1] and a later clarification added .lower(). The student workflow demonstrated here asks chat only for that local change, then integrates and runs it manually.

def count_extensions(names: list[str]) -> dict[str, int]:
    counts = {}
    for name in names:
        parts = name.rsplit(".", 1)
        ext = "(none)"
        if len(parts) == 2 and parts[0] and parts[1]:
            ext = parts[1].lower()
        counts[ext] = counts.get(ext, 0) + 1
    return counts

names = ["L01.PDF", "L02.pdf", "README", ".env", "archive.tar.gz"]
before = names.copy()
print(count_extensions(names))
print(names == before)

The actual results are {'pdf': 2, '(none)': 2, 'gz': 1} and True. rsplit(".", 1) splits at most once from the right. Only a pair of nonempty parts supplies a valid suffix. lower() creates a string assigned to local ext; the original names remain unchanged.

Independent checks and a minimal change

For ['L01.PDF', 'L02.pdf'], the clarified contract requires {'pdf': 2}. A useful follow-up gives the input, expected result, actual result and supporting rule. Preserve the original candidate. If an exception occurs, retain its complete message and traceback.

Cover empty input, mixed case, dotfiles, multiple dots, repeated names, input preservation and separate result objects. This short check is independent of earlier runnable blocks:

def count_extensions(names: list[str]) -> dict[str, int]:
    counts = {}
    for name in names:
        parts = name.rsplit(".", 1)
        ext = "(none)"
        if len(parts) == 2 and parts[0] and parts[1]:
            ext = parts[1].lower()
        counts[ext] = counts.get(ext, 0) + 1
    return counts

cases = [
    ([], {}),
    (["L01.PDF", "L02.pdf"], {"pdf": 2}),
    (["README", ".env", "notes."], {"(none)": 3}),
    (["archive.tar.gz", ".config.json"], {"gz": 1, "json": 1}),
]
for names, expected in cases:
    before = names.copy()
    assert count_extensions(names) == expected
    assert names == before
print("4 cases and input preservation: PASS")

After a revision, rerun the new counterexample and previous checks, then read the diff. Do not change an expectation merely to match a candidate when the contract has not changed. Tests can themselves be wrong; resolve the disagreement from the task. Keep a working Git checkpoint and disclose actual tools, generated scope, changes and verification. A sample declaration is not evidence that you performed its steps.

Directory adapter: optional reading

This optional material is not a task for the Linux half of this lecture. The instructor supplies the filesystem and command-line adapter; Lecture 12 explains these APIs systematically. Save the core function above together with the following adapter as inventory.py for local execution. A browser runnable block is not a way to browse your personal filesystem.

from pathlib import Path
import sys

# Add count_extensions from the independently runnable example above.

def main() -> None:
    """Instructor-provided file and command-line adapter; see Lecture 12."""
    folder = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("sample-files")
    names = [item.name for item in folder.rglob("*") if item.is_file()]
    counts = count_extensions(names)
    for ext in sorted(counts):
        print(f"{ext}: {counts[ext]}")


if __name__ == "__main__":
    main()

From the dedicated demonstration directory:

python3 check_inventory.py
python3 inventory.py sample-files
python3 inventory.py sample-files > report.out
cat report.out

sample-files is relative to the working directory, not automatically to the script’s location. Write the report outside the scanned folder so it does not become an input on the next run. The tool only reads demonstration files; it neither renames nor deletes them. The course source includes this separate demonstration in slides/l08/demo/.

Additional exception-free counterexample

This retained instructor-authored candidate is distinct from the actual first DeepSeek proposal above.

def suffix(name):
    return name.split(".")[-1].lower()

print(suffix("README"))
print(suffix("archive.tar.gz"))

README becomes readme rather than the required no-suffix category.

Complete suffix-handling reference

def suffix(name):
    parts = name.rsplit(".", 1)
    if len(parts) == 2 and parts[0] and parts[1]:
        return parts[1].lower()
    return "(none)"

for name in ["L01.PDF", "README", ".env", "archive.tar.gz"]:
    print(name, suffix(name))

Regression checks should also cover empty lists, repeated names, input preservation and independent result dictionaries.

Further reading 1. Evidence before acceptance

L07 asked what a recursive function promises, how its calls make progress and whether its results are correct. Those questions are just as useful when the implementation comes from a classmate, a website or an AI assistant.

Suppose the supplied code is short, readable and passes the example. What is still unknown? Perhaps it modifies an input that should remain unchanged. Perhaps the example omits an empty input. Perhaps the result is right but the amount of work is unsuitable for the actual data.

The candidates in this lecture are constructed teaching examples, not verbatim transcripts from a particular model. Their purpose is to make the review process observable.

Generated text is not an execution log

A token is a unit used to process text: it may represent a word, part of a word or punctuation, rather than exactly one character. A language model generates subsequent tokens from its context. A code block and a convincing explanation do not, by themselves, show that the code was executed.

At Level 2, a coding agent may also operate on the project, run programs and execute tests. At Level 1 you edit and run code yourself. Inspect what was actually done:

EvidenceWhat it supportsWhat it does not establish
A candidate and an explanationOne proposed implementation and rationaleThat it ran or meets every requirement
A recorded input and outputThe behavior of that executionCorrectness for all inputs
Test cases and their resultsWhether those checks passedComplete coverage of possible defects

Acceptance needs the requirement, the implementation and the available evidence together. The tool can contribute to the work; the person submitting the code must still understand the adopted implementation.

Three useful failure checks

Check the API. A string has no reverse() method:

s = "hello"
print(s.reverse())

The result is an AttributeError. The existence of list.reverse() does not imply a corresponding string method. Check the documented receiver type, arguments, result and side effects.

Slicing produces a reversed string without changing the original:

s = "hello"
print(s[::-1])
print(s)

Check the input contract. This implementation handles a nonempty list, but division by zero occurs on an empty one:

def average(nums: list[float]) -> float:
    return sum(nums) / len(nums)

print(average([1, 2, 3]))
print(average([]))

If the contract excludes empty lists, that input is outside its domain. If the task requires empty input to be accepted, specify the behavior before changing the function. The candidate’s current behavior is not a substitute for that decision.

Check the runtime. Syntax and library interfaces depend on versions. This is Python 2 syntax, not valid code for the course’s Python 3.13 environment:

# Python 2 syntax, not valid in the course's Python 3 environment.
print "hello"

The example illustrates a compatibility check; it is not a claim that present-day models routinely produce Python 2.

After these checks, there are still performance and readability questions. A program can return the right answer and remain expensive to run or difficult to review.

Further reading 2. Specify the function, not just its name

Interface and behavior are different

“Merge two lists, sorted” leaves several decisions open. Are the inputs already sorted? Should duplicates survive? May the function change either input?

A useful starting point is an interface plus a behavioral contract:

def merge_sorted(a: list[int], b: list[int]) -> list[int]:
    """Merge two ascending integer lists into a new ascending list.
    Keep duplicates. Do not modify either input.
    """
    ...

The annotation describes input and result types. The docstring states ordering, multiplicity and mutation requirements. This is a specification excerpt, not a completed implementation: ... does not perform the merge.

Python does not enforce type hints automatically. This call still concatenates strings:

def twice(x: int) -> int:
    return x + x

print(twice("ab"))

A type checker or an editor can use the annotation to flag a problem. The annotation alone does not reject the call or verify the result’s behavior.

Mutation is part of the contract

Predict both printed lists:

def without_two(items):
    result = items
    result.remove(2)
    return result

values = [1, 2, 3, 2]
print(without_two(values))
print(values)

Both lines print [1, 3, 2]. Assignment did not copy the list, and remove() removed only the first match.

For a different contract—remove all matches, return a new list and leave the input unchanged—a comprehension is suitable:

def remove_all(items: list[int], value: int) -> list[int]:
    return [x for x in items if x != value]

values = [1, 2, 3, 2]
print(remove_all(values, 2))
print(values)

Check both the result and the input:

def remove_all(items: list[int], value: int) -> list[int]:
    return [x for x in items if x != value]

values = [1, 2, 3, 2]
before = values.copy()
result = remove_all(values, 2)
print(result == [1, 3])
print(values == before)

Give enough information to review the answer

A checkable request normally includes:

  • a task, signature and input domain;
  • ordering, duplicates, result type and allowed side effects;
  • the Python version, permitted libraries and known syntax;
  • normal and boundary examples with expected results;
  • relevant input sizes and performance requirements.

For example:

I wrote the word-counting loop. This is the current local preprocessing:
word = token.lower()
The rule says to strip only . , ! ? ; : from both ends of each word.
Why does "cat." remain a different key from "cat"?
Explain and suggest only the change to this preprocessing line.
Do not write the full count_words function; I will integrate and test it.

This is not a universal prompt template. The punctuation rule, for instance, must match the actual task. Boundary examples make requirements explicit; they do not guarantee an error-free response.

Request small, reviewable changes

Decomposition connects directly to L07. At Level 1, write the main implementation yourself and ask about a specific condition, statement or local block when needed. Delegating the entire solution through a sequence of requests still crosses the complete-code boundary.

For a failure report, record enough to reproduce it:

Function: max_gap
Input: [7]
Expected result: 0, as specified for fewer than two elements
Actual result: ValueError from max([])
Runtime: Python 3.13
Relevant code: the construction of gaps and the final max(gaps)

“Wrong, try again” discards the useful evidence. An assistant’s diagnosis also needs checking; it is not authoritative just because it sounds plausible.

Further reading 3. Readability as part of review

Recover the behavior before refactoring

This function works on the example, but its names and nesting hide its purpose:

def f(l, D):
    R = []
    for i in range(0, len(l)):
        if l[i] in D:
            if D[l[i]] > 0:
                R.append(l[i])
    return R

print(f(["a", "b", "c"], {"a": 1, "b": 0, "c": 5}))

Its task is to retain keys whose counts are positive, keeping input order and repeated keys. Naming that behavior makes review easier:

def filter_positive(
    keys: list[str], counts: dict[str, int]
) -> list[str]:
    """Keep present keys with positive counts, in input order."""
    return [k for k in keys if counts.get(k, 0) > 0]

print(filter_positive(["a", "b", "c"], {"a": 1, "b": 0, "c": 5}))
print(filter_positive(["a", "x", "a"], {"a": 2}))

The type hints and docstring clarify this interface; their absence was not automatically a PEP 8 violation. Shorter code is not enough: check that the refactoring preserves the intended behavior.

PEP 8: conventions, not a proof

PEP 8 discusses readability and consistency. It is a style guide, not a guarantee of functional correctness.

TopicCommon convention
IndentationFour spaces per level; do not mix tabs and spaces
Line lengthNormally 79 for code, 72 for comments/docstrings; a team may agree on another limit
Blank linesTwo between top-level function/class definitions; one between class methods
Functions and variablessnake_case, such as student_count
ConstantsUPPER_CASE, such as MAX_RETRY
ClassesCapWords, such as BankAccount; class syntax comes in L09
Modules and packagesShort lowercase names, such as utils or mypackage
Internal useA leading underscore, such as _helper; this is not access enforcement
Confusing namesAvoid single-letter l, O and I
Built-in namesAvoid shadowing list, str, sum or type when their built-in meaning is needed

Continuation lines may align with an opening delimiter or use a hanging indent:

result = some_function(first_argument,
                       second_argument)

result = some_function(
    first_argument,
    second_argument,
)

These are formatting excerpts, not standalone runnable programs. Either side of a binary operator can be a line-break point if the local style is consistent; PEP 8 suggests breaking before the operator in new code:

income = (gross_wages
          + taxable_interest
          - ira_deduction)

Avoid extra whitespace inside brackets and before commas or colons. Keep spaces around assignment and comparison operators. For mixed-precedence expressions, the guide permits judgment about spacing to expose grouping. Spacing does not change precedence, and a style issue need not be a syntax error.

# Recommended layout.
spam(ham[1], {eggs: 2})
if x == 4:
    print(x, y)

# Legal in suitable contexts, but not the recommended layout.
spam( ham[ 1 ], { eggs: 2 } )

Unannotated parameter defaults and keyword arguments have no spaces around =. Annotated defaults do:

def f(x, y=2):
    ...


def g(x: int, y: int = 2) -> int:
    ...

Three tools, three kinds of work

WorkExampleWhat it checks or changes
Formattingruff format file.py, or BlackLayout and whitespace
Lintingruff check file.pyEnabled static rules, such as unused imports or undefined names
Behavioral testingExecute chosen cases and compare resultsSpecific requirements

Ruff only checks the selected rules. Its defaults are not “all of PEP 8”, and an empty diagnostic report does not certify the program’s logic. A formatter will not decide what averaging an empty list should mean.

python3 -m pip install ruff
ruff check file.py
ruff format --check file.py     # check without rewriting

The VS Code Ruff extension is another route. If pip reports an externally managed environment, do not force an administrator-level installation; use the course’s Python installation or the editor extension. L10 covers virtual environments. Black is an alternative formatter, not an additional mandatory installation.

Further reading 4. Tests, counterexamples and debugging

Expected results come from the contract

For max_gap, specify an integer list, the largest adjacent gap after sorting, a result of zero for fewer than two elements and unchanged input.

InputExpectedPurpose
[3, 9, 1, 15]6An unsorted ordinary input
[]0Empty input
[7]0One element
[2, 2, 2]0Repeated values
[-3, -1, -10]7Negative values

The candidate omits the short-input case:

def max_gap(nums: list[int]) -> int:
    ordered = sorted(nums)
    gaps = [ordered[i + 1] - ordered[i]
            for i in range(len(ordered) - 1)]
    return max(gaps)

print(max_gap([7]))

The list of gaps is empty, so max() raises ValueError. After a correction, recheck the failure and previously working cases:

def max_gap(nums: list[int]) -> int:
    if len(nums) < 2:
        return 0
    ordered = sorted(nums)
    gaps = [ordered[i + 1] - ordered[i]
            for i in range(len(ordered) - 1)]
    return max(gaps)

for nums, expected in [([3, 9, 1, 15], 6), ([7], 0), ([], 0)]:
    before = nums.copy()
    got = max_gap(nums)
    print(got == expected, nums == before)

This is a regression check. A finite suite can expose defects, but generally does not prove correctness on every input. Read the bounds, conditions and state changes as well.

Useful review dimensions include empty input, one element, repeated values, zero and negatives, scale, output order and mutation. Wrong-type handling is contract-dependent: do not silently require a function specified for integer lists to accept every possible object.

Read the traceback as a record

ExceptionTypical meaning or example
SyntaxErrorInvalid syntax, such as a missing colon
IndentationErrorInvalid indentation structure
NameErrorA name has not been defined
TypeErrorAn operation or call rejects the type/argument form
ValueErrorA value violates the interface requirement, such as int("abc")
IndexErrorA sequence index is outside its range
KeyErrorDirect indexing requests a missing dictionary key
ZeroDivisionErrorA divisor is zero
AttributeErrorAn attribute or method is unavailable on the object
RecursionErrorExecution exceeds the interpreter’s recursion-depth limit

A traceback contains call locations and the failing statement; its final line normally names the exception and message. Source lines and caret markers may intervene, so “the second-to-last line is the error location” is not a reliable rule. A recursion-depth failure can come from faulty recursion or a valid but too-deep input. Exception-handling syntax comes in L11.

Observe the state of a failing case

The following candidate is meant to return the longest run of ones from a list containing only zero and one; empty input returns zero:

def longest_run(bits: list[int]) -> int:
    current = 0
    best = 0
    for bit in bits:
        if bit == 1:
            current += 1
        else:
            best = max(best, current)
            current = 0
    return best

print(longest_run([1, 1, 0]))
print(longest_run([1, 1, 0, 1, 1, 1]))

Both calls return 2, but the second should return 3. Just before return, best=2 and current=3. The final run never met a zero, so it was not included in best.

A temporary print can expose these values. Alternatively, pause on the return statement in VS Code:

  1. Enable the Python and Python Debugger extensions and select the course interpreter.
  2. Click the gutter beside the return statement to set a breakpoint.
  3. Start from Run and Debug; execution pauses before that statement runs.
  4. Inspect the selected frame’s variables, or add best and current to Watch.
ActionCommon shortcutBehavior
ContinueF5Resume until a breakpoint or completion
Step OverF10Execute the line without entering the called function
Step IntoF11Enter a function called on the line
Step OutShift+F11Finish the current function and return to the caller

The operating system, keyboard and custom bindings can change shortcuts; some keyboards need Fn. The toolbar action names are another way to operate the debugger. Watch evaluates in the selected context when execution pauses, not continuously during execution.

For this contract, include the unfinished run when returning:

def longest_run(bits: list[int]) -> int:
    current = 0
    best = 0
    for bit in bits:
        if bit == 1:
            current += 1
        else:
            best = max(best, current)
            current = 0
    return max(best, current)

for bits in [[], [0], [1], [1, 1, 0], [1, 1, 0, 1, 1, 1]]:
    print(bits, longest_run(bits))

Select a recursive frame

When L07’s factorial(3) has entered factorial(1), three calls are active with local n values 1, 2 and 3. Selecting the outer frame shows its own n=3; the inner call has not overwritten it.

Print is convenient for a few values or a batch log. A debugger is convenient for pausing, stepping and selecting frames. Both remain useful; neither is a universal replacement for the other.

Further reading 5. Correct answers at the required scale

This deduplication preserves first-occurrence order:

def dedup(items: list[int]) -> list[int]:
    result = []
    for x in items:
        if x not in result:
            result.append(x)
    return result

print(dedup([3, 1, 3, 5, 1]))

Reading the membership check reveals repeated searches through a growing list. An explicit search lets us count how many existing elements are examined:

def dedup_counted(items: list[int]) -> tuple[list[int], int]:
    result = []
    comparisons = 0
    for x in items:
        found = False
        for old in result:
            comparisons += 1
            if old == x:
                found = True
                break
        if not found:
            result.append(x)
    return result, comparisons

for n in [10, 100, 1000]:
    result, checks = dedup_counted(list(range(n)) * 2)
    print(n, checks)

For this particular input—n distinct elements appearing twice in the same order—the explicit comparison count is n². That is not a measurement of CPython machine instructions, nor a claim that all distributions use exactly n² checks.

A set can record seen integers while a list retains result order:

def dedup(items: list[int]) -> list[int]:
    seen = set()
    result = []
    for x in items:
        if x not in seen:
            seen.add(x)
            result.append(x)
    return result

for items in [[], [3, 1, 3, 5, 1], [-1, 0, -1]]:
    before = items.copy()
    print(dedup(items), items == before)

Set lookup has a favorable average cost; it is not guaranteed to be a single step. This interface uses hashable integers, not arbitrary nested lists.

A timing comparison must use comparable data, environments and measurement methods, and first establish equivalent results. Hardware, runtimes and other work affect elapsed seconds. One observed speedup is not a universal ratio. L11 develops the time/space analysis formally.

Further reading 6. Review, acceptance and disclosure

Four candidates, different questions

The lecture has used four review tasks:

  1. items.remove_all(value): determine API availability through documentation and execution.
  2. max_gap: expose an omitted boundary with a short input.
  3. dedup: identify repeated list search and check the amount of work.
  4. f(l,D): recover the behavior, improve readability and check equivalence.

Do not assume that every candidate has exactly one defect. Do not classify behavior outside the stated domain as a defect without first checking the requirement.

A precise IPv4 exercise

Lab08 uses is_valid_ipv4(s: str) -> bool. Its string inputs follow this strict dotted-decimal contract:

  • exactly four parts separated by .;
  • ASCII digits 0 through 9 only, each value in 0–255;
  • the single digit 0 is allowed; other leading-zero forms are not;
  • no empty parts, signs or surrounding/internal whitespace.

Write independent tests before inspecting a candidate. Valid cases can include 0.0.0.0, 192.168.1.1 and 255.255.255.255. Invalid cases should cover part count, range, leading zeros and character restrictions. The full corrected lab solution is not supplied here.

A documented conversion may accept more than the task permits, and a familiar character predicate may be broader than expected:

for s in ["01", "+1", " 1", "1"]:
    print(repr(s), int(s))

for s in ["1", "1", "²"]:
    print(repr(s), s.isdigit())

The ASCII restriction is explicit in this exercise. Python’s ipaddress documentation states the four-part and leading-zero requirements but does not spell out that character restriction on the referenced page. Keep documented claims, observed behavior and the task’s own contract distinct.

Division of work and safe use

StageWork to retain
Understand and decomposeDecide goals, inputs, outputs and interfaces
ImplementOwn the main code; ask chat for limited code suggestions
ReviewUnderstand state, unfamiliar APIs and side effects
TestDesign checks from the contract; tools may add suggestions
CorrectKeep the counterexample, original candidate and change scope
AcceptExplain the evidence and take responsibility for the submitted code

Work out the task before implementing it. Check unfamiliar constructions against documentation and the runtime. Make the function work, then decide what the required input sizes demand; avoiding premature optimization does not excuse an obviously unsuitable design.

Remove personal information, tokens, passwords and keys before sending material to an external tool. Do not run unfamiliar file operations, installations or network requests without inspecting them. Keep teaching failures small and within an environment you control.

An AI usage declaration

At Level 1, use chat for local code assistance and debugging, not complete assigned solutions or coding-agent project operations. Disclose the tool, local generated or revised scope and the checks you actually ran. This is a format example, not a real usage log:

AI usage declaration:
Tool and model: record what was actually used; say if the model is unknown.
Generated/revised scope: for example, the single suffix-normalization line.
Manual changes: identify the functions, conditions or blocks changed.
Verification: actual inputs, expected and observed results; input preservation.
Independent work: identify the parts completed without AI.

“An AI said it was correct” is not a test record. If an online tool was unavailable and the instructor’s candidate was used, disclose that source rather than inventing a conversation. If no AI was used, state that explicitly.

Solo assignment 1 remains Level 0, covers ordinary and adjacent-peg Hanoi and is worth 5% of the semester grade. Its deadline is on the submission page. This week’s Level 1 does not change that assignment’s rule. Do not send its implementation to an AI for generation or rewriting; this lecture uses separate classroom examples such as factorial.

Further reading 7. Reference material and legacy exercises

Imports, comments and layout details

Keep these as reference checks:

  • Put imports after the module docstring; group standard library, third-party and project imports with blank lines. Avoid wildcard imports whose names may shadow others.
  • Prefer implicit continuation inside brackets; avoid trailing whitespace and multiple statements crowded onto a line.
  • A one-element tuple needs a comma; use trailing commas in multiline constructs according to the guide and project conventions.
  • Comments should explain reasons and stay synchronized with the code. Docstrings state purpose, parameters and results.
  • Python 3 source defaults to UTF-8. Keep quoting consistent without treating one quote style as uniquely valid.
  • Class, inheritance, public-interface and exception-naming conventions become useful as later lectures introduce those constructs; they are not prerequisites now.

This import grouping is a formatting excerpt; it does not assume that the third-party and project modules are installed:

import os
import sys

import numpy as np

from mypackage import helper

Consult the full PEP 8 for details. The Zen of Python is available through:

import this

Tim Peters wrote these principles. Readability and explicitness describe design choices, not a line-by-line correctness certificate.

Leap years: equivalent implementations

For positive Gregorian years, the rule is divisibility by four but not one hundred, or divisibility by four hundred. These three structures express that rule:

def leap_separate(year: int) -> bool:
    if year % 4 == 0 and year % 100 != 0:
        return True
    if year % 400 == 0:
        return True
    return False


def leap_branches(year: int) -> bool:
    if year % 4 == 0 and year % 100 != 0:
        return True
    elif year % 400 == 0:
        return True
    else:
        return False


def leap_boolean(year: int) -> bool:
    return (year % 4 == 0 and year % 100 != 0
            or year % 400 == 0)

for year in [1900, 2000, 2024, 2100]:
    print(year, leap_separate(year), leap_branches(year), leap_boolean(year))

Primes: domain and stopping bounds

A prime is an integer greater than one with exactly two positive divisors. Both legacy versions omitted the n<2 guard. Normal positive samples did not expose it.

def prime_slow(n: int) -> bool:
    if n < 2:
        return False
    for divisor in range(2, n):
        if n % divisor == 0:
            return False
    return True


def prime_fast(n: int) -> bool:
    if n < 2:
        return False
    divisor = 2
    while divisor * divisor <= n:
        if n % divisor == 0:
            return False
        divisor += 1
    return True

for n in [-1, 0, 1, 2, 3, 4, 9, 97]:
    print(n, prime_slow(n), prime_fast(n))

A factor above the square root is paired with one below it. Include the equality boundary for perfect squares; integer multiplication avoids using a floating approximation for that decision.

Digit squares and happy numbers

For nonnegative integers, digit-square sums can be computed by arithmetic iteration, recursion or string traversal:

def squares_loop(n: int) -> int:
    total = 0
    while n > 0:
        total += (n % 10) ** 2
        n //= 10
    return total


def squares_recursive(n: int) -> int:
    if n < 10:
        return n * n
    return (n % 10) ** 2 + squares_recursive(n // 10)


def squares_text(n: int) -> int:
    total = 0
    for ch in str(n):
        total += int(ch) ** 2
    return total

for n in [0, 2, 9, 19, 29]:
    print(n, squares_loop(n), squares_recursive(n), squares_text(n))

The old recursive base case returned n: it gave 2 rather than 4 for input 2, although the example 19 happened to work. Small boundary checks matter. Recursion also has a depth limit.

A positive integer is happy if repeated digit-square replacement reaches one. An already visited state means that the deterministic process will repeat:

def sum_digit_squares(n: int) -> int:
    total = 0
    while n > 0:
        total += (n % 10) ** 2
        n //= 10
    return total


def is_happy(n: int) -> bool:
    seen = set()
    while n != 1:
        if n in seen:
            return False
        seen.add(n)
        n = sum_digit_squares(n)
    return True

print(is_happy(19))
print(is_happy(2))

The set records which states occurred, not their multiplicities. That is enough for cycle detection. Zero is outside the positive-input definition.

Runs: consecutive positions matter

The legacy nested-while implementation also finds the longest run of ones. The outer index skips a run already traversed by the inner loop, so nesting alone does not imply quadratic work.

def longest_run(bits: list[int]) -> int:
    best = 0
    i = 0
    while i < len(bits):
        if bits[i] == 1:
            j = i
            while j < len(bits) and bits[j] == 1:
                j += 1
            best = max(best, j - i)
            i = j
        else:
            i += 1
    return best

for bits in [[], [0], [1], [1, 1, 0, 1, 1, 1]]:
    print(bits, longest_run(bits))

A continuous increasing run compares adjacent positions and requires strict increase. Equal values end the current run. It cannot skip a value to form an arbitrary subsequence:

def longest_increasing_run(nums: list[int]) -> int:
    if not nums:
        return 0
    current = 1
    best = 1
    for i in range(1, len(nums)):
        if nums[i] > nums[i - 1]:
            current += 1
        else:
            current = 1
        best = max(best, current)
    return best

for nums in [[], [1, 3, 5, 4, 7], [2, 2, 2], [1]]:
    print(nums, longest_increasing_run(nums))

Anagrams require multiplicities

For lowercase ASCII strings, matching character counts establishes an anagram. Counting and sorting are two possible implementations:

def char_counts(s: str) -> dict[str, int]:
    counts = {}
    for ch in s:
        counts[ch] = counts.get(ch, 0) + 1
    return counts


def anagram_counts(s: str, t: str) -> bool:
    return char_counts(s) == char_counts(t)


def anagram_sorted(s: str, t: str) -> bool:
    return sorted(s) == sorted(t)

for s, t in [("anagram", "nagaram"), ("rat", "car"), ("aab", "abb"), ("", "")]:
    print(s, t, anagram_counts(s, t), anagram_sorted(s, t))

set("aab") equals set("abb"), but the multiplicities differ. Sets do not preserve the information this task needs.

Simplification and development habits

For an actual bool parameter, if flag is straightforward. For a general object, truthiness is not always equivalent to flag == True. Similarly, L05’s += and + examples show that a shorter spelling can change mutation behavior.

Retain useful habits from the old lecture: plan the task and intermediate state, separate functions by interfaces, test a completed small piece, reuse computed results, name constants, check unfamiliar APIs and preserve failures. Function length, loop depth or brevity do not prove correctness; append() also has appropriate uses.

Summary

Acceptance combines a contract, code review, documentation and runtime checks, targeted tests, mutation checks and scale considerations. A tool run is evidence; a finite test suite is not generally a proof for every input.

Formatters, linters, print and debuggers solve different problems. Preserve the original candidate, failures, modifications and regression results so the acceptance decision can be explained.

L09 asks how the data and operations of similar records can be organized together using classes.

Exercises

  1. Investigate the candidate below on both nonempty and empty input. Revised contract: return an empty string for empty input; when frequencies tie, return the first word encountered. Correct it and test that contract.
    def most_common(words: list[str]) -> str:
        counts = {}
        for word in words:
            counts[word] += 1
        return max(counts, key=counts.get)
  2. Specify a function converting scores into letter grades. Include a signature, behavior and at least three examples. Decide score bounds, grade thresholds, empty-input behavior and mutation.
  3. Write at least six tests from this lecture’s IPv4 contract before examining an AI or instructor-provided candidate. Record expected/observed results and a correction.
  4. Compare recursive and iterative versions of the classroom factorial() function, including n=0. AI assistance may follow this week’s policy; do not ask it to rewrite Solo assignment 1.
  5. Run Ruff on your own Labs 1–7 code and explain three unfamiliar diagnostics. If fewer exist, use Lab08’s practice file rather than inventing warnings.
  6. Read PEP 8’s “A Foolish Consistency is the Hobgoblin of Little Minds” and summarize how local consistency and readability differ from mechanically applying rules.

See Lab08 for the actual tasks, records and submission instructions. The policy is on the AI policy page.