The Missing Semester of Your CS Education

BSCS and BSIT students — and any self-taught programmer — who want real fluency with the shell, a serious text editor, Git, and the rest of a working developer’s toolkit can take “The Missing Semester of Your CS Education,” a free 9-lecture MIT course co-taught by Anish Athalye, Jon Gjengset, and Jose Javier Gonzalez Ortiz. It exists because, as the instructors put it, “classes teach you all about advanced topics within CS … but there’s one critical subject that’s rarely covered, and is instead left to students to figure out on their own: proficiency with their tools.” The course covers the shell and command-line environment, development tools and editors, debugging and profiling, version control with Git, packaging and shipping code, AI-assisted and agentic coding, engineering communication, and code quality.

The 2026 offering is a substantially revised iteration of the not-for-credit class MIT students first ran in 2019 and 2020, taught each January during MIT’s Independent Activities Period. All lecture notes, exercises, and recorded video are hosted free, directly by MIT, under the CC BY-NC-SA 4.0 licence — no PDF, no sign-up, and no paywall stand between you and the material. Rather than teaching AI tools as a single bolted-on lecture, the course folds practical AI-tool usage into whichever lecture it’s actually relevant to, from AI-powered autocomplete in the editors lecture to a full dedicated lecture on coding agents — making it one of the most current, practically-focused free resources a CS student can add alongside a formal degree program.

Book Overview

CoursePractical CS Tools & Developer Workflow (not tied to one specific course — run as a standalone MIT IAP class)
Degree ProgramsUseful alongside any BSCS/BSIT coursework as a practical skills supplement, not a formal graded course textbook
LevelUniversity level — assumes only basic prior programming experience; no theory-of-computation or CS-theory prerequisite
Edition2026 Edition (MIT Independent Activities Period, January 2026) — a substantially revised iteration of the not-for-credit class first taught at MIT in 2019 and run again in 2020
Author(s)Anish Athalye, Jon Gjengset, and Jose Javier Gonzalez Ortiz (MIT alumni, course co-instructors)
Structure9 lectures in the 2026 offering, each a standalone written page plus a recorded video: Course Overview + The Shell, Command-line Environment, Development Environment and Tools, Debugging and Profiling, Version Control and Git, Packaging and Shipping Code, Agentic Coding, Beyond the Code, and Code Quality
ExercisesEach of the 9 lectures ends with its own hands-on exercise set (terminal tasks, small debugging/profiling programs to compile and run, Vim drills, regex practice, etc.); the course does not publish one combined exercise count
LanguageEnglish
LicenseCC BY-NC-SA 4.0
FormatFree web pages (HTML lecture notes) plus recorded lecture videos, hosted directly by MIT; there is no downloadable PDF or EPUB edition — everything is read directly on the official site

Chapter List

Lecture 1: Course Overview + Introduction to the Shell

Difficulty: Easy · MIT IAP 2026 · Course Introduction · Key topics: the shell vs. the terminal, prompt anatomy, cd & paths, $PATH and which, man/tldr, core utilities (cat, grep, sed, find, awk)

Opens by introducing the course itself — a not-for-credit, nine-lecture MIT IAP class co-taught by Anish, Jon, and Jose, all ex-MIT students who started the class as students themselves, that folds AI-tool awareness into every lecture rather than teaching it as one standalone topic — before turning to its first real subject, the shell. Defines the shell as the textual interface reached through a terminal, distinguishes bash/zsh/fish on Linux and macOS from cmd/PowerShell on Windows, and argues that a textual interface unlocks capabilities no button-driven GUI, voice interface, or LLM chat window exposes. Walks through navigating the shell — the prompt’s user/host/directory anatomy, the built-in cd command, the special . and .. path components, and absolute versus relative paths — and shows how the $PATH environment variable and the which command let the shell locate a program like echo or date before running it. Surveys the core utilities on $PATH — cat, sort, uniq, head/tail, grep, sed, find, and awk — building to a single chained ssh | journalctl | grep pipeline that shows several small tools composing to solve one real diagnostic task.

Key Points:

  • A terminal is the visual interface to a shell; bash is the default on most Linux/macOS systems, while Windows uses cmd/PowerShell (or the Windows Subsystem for Linux for a Unix-style shell).
  • man <command> and --help give built-in documentation; tldr shows common real-world usage examples instead of a full manual.
  • cd is a shell builtin, not a separate program on $PATH — it has to run inside the shell process itself to change that shell’s current working directory.
  • Absolute paths start with / and are unambiguous; relative paths resolve from the current working directory, using . (this directory) and .. (parent directory).
  • $PATH lists, in order, the directories the shell searches to resolve a bare command name to an executable file; which <cmd> reports exactly which file will run.
  • Core utilities: cat prints a file, sort/uniq order and dedupe lines, head/tail show the first/last lines, grep finds regex matches, sed does stream text substitution, and awk extracts columns from structured text.
  • find locates files by name, type, or age (-type, -name, -mtime), and its -exec option runs a command on every match, with {} replaced by the file path and a trailing \; to terminate the command.

Practice Tip: Open a terminal and run echo $PATH, then which ls, and confirm the directory which names is actually one of the colon-separated entries in $PATH — seeing the lookup happen yourself is the fastest way to make $PATH resolution concrete instead of abstract.

Common Mistake: Assuming find‘s -exec runs once for all matched files together — each match actually triggers a separate invocation of the command when -exec is terminated with \;, so find . -name “*.py” -exec grep -l “TODO” {} \; runs grep once per matching file, not once with every filename passed together.

Important Questions:

  • What is $PATH, and how does the shell use it to run a bare command like date? $PATH is an environment variable holding a colon-separated list of directories; when you type a command that isn’t a shell builtin, the shell searches those directories in order for an executable file with that name and runs the first match it finds — which date reports exactly which file gets executed.
  • Why can’t which cd find a file for cd, and what does that reveal about it? cd is implemented as a shell builtin rather than a separate executable on $PATH, because changing the current working directory has to happen inside the running shell process itself — a child process launched to do the work could change its own working directory but never its parent shell’s, so cd has to be built into the shell rather than shelled out to.

Lecture 2: Command-line Environment

Difficulty: Medium · MIT IAP 2026 · Shell Scripting · Key topics: arguments & flags, globbing, stdin/stdout/stderr, pipes & redirection, quoting, command & process substitution, environment variables

Explains that shell scripting is a genuine programming language built entirely around running programs and connecting them together, governed less by strict syntax rules than by dense conventions every well-behaved command-line program follows. Covers how programs receive arguments ($1…$9, $@, $#), how single- and double-dash flags like -a and --all modify behavior and can be grouped (-la), and how shell globbing patterns (*, ?, {a,b,c}) let one command like rm *.py expand to match many files without an explicit loop. Introduces the three standard streams every program has — stdin, stdout, stderr — the | pipe operator that runs an entire pipeline concurrently rather than one stage at a time, and the redirection operators (>, >>, 2>, &>, <) for sending streams to and from files. Distinguishes single-quoted strings (fully literal) from double-quoted strings (which expand variables and command substitutions), shows $(command) for capturing a command’s stdout into a variable and <(command) process substitution for feeding a command’s output to something expecting a filename, and closes by introducing environment variables — inherited by child processes, conventionally ALL_CAPS, and set per-command or exported with export.

Key Points:

  • Arguments are always plain strings; $1..$9 access individual positional arguments inside a script, $@ all of them, $# the count, and $0 the program name.
  • Flags conventionally use a single dash for short forms (-a) and a double dash for long forms (--all); short flags can be grouped (-la means -l -a).
  • Globs are expanded by the shell before the program ever runs; * matches any string, ? matches exactly one character, and {a,b,c} expands into multiple literal arguments.
  • A pipeline (cmd1 | cmd2 | cmd3) starts all commands at once and streams data between them concurrently — it does not run each command to completion before starting the next.
  • > overwrites a file with stdout, >> appends, 2> redirects stderr, &> redirects both, and < feeds a file to stdin.
  • Single quotes ('...') are fully literal and never expand variables; double quotes ("...") expand $variable, command substitution, and escape sequences.
  • $(command) captures a command’s stdout into a variable (command substitution); <(command) writes a command’s output to a temporary file and substitutes its path (process substitution), useful for tools like diff <(ls src) <(ls docs) that expect filenames rather than piped input.

Practice Tip: Run diff <(ls dirA) <(ls docs) on two real directories to see process substitution work, then rerun it as ls dirA > /tmp/a; ls docs > /tmp/b; diff /tmp/a /tmp/b to see the two temp-file steps <() is quietly doing for you.

Common Mistake: Writing foo = bar expecting to assign a shell variable — the spaces make the shell parse it as calling a program named foo with arguments = and bar instead; variable assignment must be written with no spaces around the =, as foo=bar.

Important Questions:

  • Why does a pipeline like cat file | grep pattern | sort start all three programs immediately instead of running them one after another? The shell spawns all three processes at nearly the same time and connects the stdout of each to the stdin of the next via the | operator, so data streams through the pipeline concurrently rather than materializing as an intermediate file between stages; a process listing taken mid-pipeline shows every stage already running rather than waiting for the previous one to finish.
  • What is the difference between $(command) and <(command), and when would you reach for each? $(command) is command substitution — it runs command, captures its stdout as text, and substitutes that text directly into the surrounding shell command (e.g. files=$(ls)); <(command) is process substitution — it runs command, writes its output to a temporary file-like object, and substitutes that object’s path, for cases like diff <(ls src) <(ls docs) where the receiving program expects a filename argument rather than piped stdin.

Lecture 3: Development Environment and Tools

Difficulty: Medium · MIT IAP 2026 · Editors & Tooling · Key topics: IDEs vs. terminal workflows, Vim modal editing, movement/edit/modifier commands, Language Server Protocol, AI autocomplete & inline chat

Contrasts graphical IDEs such as VS Code with terminal-based workflows built from tmux, Vim, Zsh, and language-specific CLI tools, recommending basic familiarity with both and mastery of at least one. Introduces Vim’s core idea — that its interface is itself a small, composable programming language rather than a menu of mouse-driven actions — through its modal design: Normal mode for movement and edits, Insert mode for typing text, Visual modes for selecting, and Command-line mode, switched between with <ESC> and mode-entry keys like i, v, and :. Works through Vim’s vocabulary of movement “nouns” (hjkl, w/b/e for words, 0/$ for line ends, gg/G for file ends) and editing “verbs” (d, c, y, p) that compose with those nouns and with counts (3w, 5j) and modifiers (ci(, da') to form efficient, repeatable edits, demonstrated by live-editing a broken FizzBuzz function. Closes by covering Language Server Protocol code intelligence (autocomplete, jump-to-definition, find-references, powered by servers like Pylance or gopls) and the three current forms of AI-assisted coding — comment-steered autocomplete, inline chat that edits a selected block, and coding agents, covered fully in a later lecture.

Key Points:

  • Vim is a modal editor: Normal (move/edit), Insert (type text), Replace, Visual/Visual Line/Visual Block (select), and Command-line mode, each giving the same keystroke a different meaning.
  • <ESC> always returns to Normal mode; from Normal mode, i enters Insert, v/V/Ctrl-v enter the three Visual modes, and : enters Command-line mode.
  • Movement commands (“nouns”) include hjkl, w/b/e (word forward/back/end), 0/^/$ (line start/first-non-blank/end), and gg/G (file start/end).
  • Editing commands (“verbs”) like d{motion}, c{motion}, x, y, and p compose with any motion — dw deletes a word, d$ deletes to end of line, cw changes a word.
  • Counts multiply an action (3w moves 3 words, 7dw deletes 7 words) and modifiers narrow a noun’s scope (ci( changes inside parentheses, da' deletes a single-quoted string including its quotes).
  • IDE code intelligence (autocomplete, inline docs, jump-to-definition, find-references) is powered by Language Server Protocol servers specific to each language, such as Pylance for Python or gopls for Go.
  • AI autocomplete suggests code at the cursor and is steerable with a descriptive comment; inline chat lets you select existing code and prompt a direct edit — a different interaction mode from autocomplete, which only ever completes forward from the cursor.

Practice Tip: Take the lecture’s broken FizzBuzz function and retype its five fixes yourself in Vim — o to add the missing main() call, ci" to swap a quoted string, ww plus i/a to insert around a word — the sequence only becomes muscle memory once your own fingers make the edits, not once you’ve read about them.

Common Mistake: Staying in Insert mode and using the arrow keys or Backspace for all navigation and edits — this reduces Vim to a slower version of Notepad; the efficiency only appears once you spend most of your time in Normal mode composing motions (nouns) with editing commands (verbs) instead of retyping text.

Important Questions:

  • What does ci( do, and why is it faster than selecting the same text manually in Visual mode first? ci( means “change inside the current pair of parentheses” — the i modifier selects only the content between the nearest enclosing ( and ) (excluding the parentheses themselves), deletes it, and drops you into Insert mode to type the replacement, all as one composed command; doing the same in Visual mode would require manually positioning the cursor and selecting the exact span first.
  • Why does the lecture describe Vim’s interface as “a programming language” rather than just a set of keyboard shortcuts? Because Vim’s keystrokes are composable the way a programming language’s grammar is: movement commands (“nouns”) combine with editing commands (“verbs”), counts, and modifiers in predictable, orthogonal ways — d (delete) plus any motion deletes that motion’s span, so learning one verb and one noun set lets you construct far more edits than memorizing an equivalent list of fixed keyboard shortcuts would allow.

Lecture 4: Debugging and Profiling

Difficulty: Hard · MIT IAP 2026 · Debugging & Performance · Key topics: print/log debugging, gdb/lldb, rr record-replay, strace & eBPF, AddressSanitizer/Valgrind, perf & flame graphs, hyperfine benchmarking

Opens with Kernighan’s line that the most effective debugging tool is still careful thought paired with judiciously placed print statements, then builds outward to interactive debuggers like gdb and lldb, which set breakpoints, step through code, and inspect variables in any native binary. Introduces record-replay debugging with rr, which records a Linux program’s execution deterministically so a Heisenbug — one that changes behavior under observation, such as a race condition — can be replayed and stepped backward with commands like reverse-continue until the exact corrupting line is found. Covers system-call tracing with strace/dtruss and kernel-level tracing with bpftrace/eBPF, plus memory debugging with compiler sanitizers (AddressSanitizer, ThreadSanitizer) and Valgrind for catching buffer overflows, use-after-free, and data races. Turns to profiling — timing with the time command’s Real/User/Sys breakdown, resource monitoring with htop/iotop/lsof/ss, and CPU profiling with the sampling profiler perf (visualized as flame graphs) and Valgrind’s tracing profiler Callgrind — and closes with hyperfine for statistically rigorous command-line benchmarking.

Key Points:

  • Print/log debugging remains the most practical first approach; converting a successful print-debugging session into permanent log statements captures diagnostic information for the next time a similar bug appears.
  • gdb (compiled languages) and language-specific debuggers like Python’s pdb let you set breakpoints (b {file}:{line}), step (step/next/finish), inspect variables (p {variable}), and view the call stack (bt).
  • rr records a Linux program’s execution deterministically, so rr replay supports reverse debugging commands like reverse-continue and reverse-step to find exactly where corrupted state came from.
  • strace/dtruss trace every system call a program makes; bpftrace (built on eBPF) traces kernel-level events with lower overhead and supports aggregations like per-syscall counts.
  • Sanitizers (AddressSanitizer, ThreadSanitizer) require recompilation but run fast enough for CI; Valgrind is slower but needs no recompilation, useful when you lack source access or can’t recompile.
  • time splits execution into Real (wall clock), User (CPU time in user code), and Sys (CPU time in the kernel) — a call that’s mostly Real but little User+Sys is dominated by waiting, not computing.
  • perf is a low-overhead sampling profiler that periodically records the call stack; flame graphs turn its raw output into an interactive, zoomable visualization of where time is spent.

Practice Tip: Compile the lecture’s own use-after-free example with gcc -fsanitize=address -g uaf.c -o uaf and run it — comparing the AddressSanitizer report against the silent, seemingly-working output of the unsanitized build makes the danger of use-after-free bugs concrete rather than theoretical.

Common Mistake: Reaching for a normal debugger before trying rr on a bug that only reproduces intermittently (a Heisenbug, a race condition, a flaky test) — re-running a plain debugger changes timing and can make the bug disappear entirely, while rr record captures one execution deterministically so rr replay can be stepped through, and backward through, as many times as needed.

Important Questions:

  • What makes rr‘s record-replay approach effective against Heisenbugs that a normal debugger session struggles with? A normal debugger session re-runs the program from scratch every time, and the act of debugging itself (breakpoints, print statements, slower single-stepping) can change timing enough to make a race condition or other timing-dependent bug disappear; rr instead records one real execution deterministically to disk, and every replay of that recording reproduces the exact same execution, letting you set watchpoints and use reverse-continue/reverse-step to walk backward from the point of failure to the exact line that caused it.
  • Why does the lecture recommend perf‘s sampling approach over Valgrind’s Callgrind for most CPU profiling? perf is a sampling profiler that periodically probes the program’s call stack (commonly every millisecond) with low overhead, making it suitable even for production workloads, while Callgrind is a tracing profiler that records exact call counts and caller/callee relationships by instrumenting every call, which is far more precise but also far slower; the lecture recommends perf for typical hot-spot finding and reserves Callgrind for when you specifically need exact call counts or cache-behavior simulation.

Lecture 5: Version Control and Git

Difficulty: Medium · MIT IAP 2026 · Version Control · Key topics: snapshots, blobs/trees/commits, content-addressing (SHA-1), references & HEAD, staging area, branching & merging, rebase

Argues that Git’s command-line interface is a leaky abstraction best learned bottom-up, starting from its data model rather than memorizing commands as magic incantations. Models Git’s history as a directed acyclic graph of immutable commits, each pointing to a top-level “tree” (a directory mapping names to blobs or further trees) and to its parent commit(s) — a set of parents rather than a single one, since a merge commit descends from more than one branch. Explains content-addressing: every object (blob, tree, or commit) is stored keyed by its own SHA-1 hash, and human-readable “references” like master and the special HEAD reference are simply mutable pointers into this otherwise-immutable object graph. Introduces the staging area as the mechanism for choosing exactly which modifications go into the next commit — for example, committing a bugfix while leaving unrelated debug print statements out of that same snapshot — before surveying the core command-line interface: git add/commit/log, branching and merging (git branch, git switch, git merge, git rebase), remotes (git fetch/pull/push), and undo commands (git commit --amend, git reset, git restore).

Key Points:

  • Git models history as a DAG of commits, not a straight line; a commit stores its author, message, a pointer to its snapshot tree, and a set of parent commits (more than one for a merge commit).
  • A blob is a file’s bytes; a tree maps names to blobs or other trees (i.e., directories); a commit is the top-level tree plus metadata — blobs, trees, and commits are all just “objects”.
  • Every object is content-addressed by its SHA-1 hash, so two identical files or trees anywhere in a repository’s history are stored only once.
  • References (like master and HEAD) are human-readable, mutable pointers to a commit hash; commits themselves are immutable, so an “edit” to history is really a new commit with references updated to point at it.
  • The staging area lets you choose exactly which changes go into the next commit — git add stages files, and only staged content becomes part of the next git commit‘s snapshot.
  • git switch <name> / git checkout -b <name> move between branches; git merge combines branches into a merge commit; git rebase replays a set of commits onto a new base instead.
  • git log --all --graph --decorate visualizes the commit DAG directly, turning the abstract “directed acyclic graph of snapshots” description into something you can actually see on a real repository.

Practice Tip: Run git cat-file -p <hash> on a tree and then on one of the blobs it lists, on any repo you already have — seeing a real tree object’s raw content-addressed pointers to its blobs and sub-trees is what makes “Git’s data model is a content-addressed graph of objects” click instead of staying an abstract diagram.

Common Mistake: Treating Git commands as a fixed list of incantations to memorize instead of asking what graph manipulation each command performs — git reset --hard <hash> and git checkout <branch> both ultimately move a reference and/or HEAD, and once you can name that operation on the underlying commit DAG, the right command is usually easy to find rather than something you have to have seen before.

Important Questions:

  • Why does Git use a set of parent commits rather than a single parent for each commit? A linear, single-parent history cannot represent the common case where two branches of independent development are later combined; Git instead lets a commit have any number of parents, so a merge commit — created when git merge combines two branches — can point back to both branches’ latest commits at once, accurately recording that its snapshot descends from both histories rather than picking just one.
  • What problem does the staging area solve that a simpler “snapshot the whole working directory” model would not? Without a staging area, every commit would have to capture the entire current state of the working directory, which is a problem when you’ve made several unrelated changes at once — for instance, implementing two separate features, or mixing a real bugfix with leftover debug print statements; the staging area lets you git add only the specific changes that belong in the next commit, so you can create one clean commit for the bugfix and a separate one for the debug cleanup, instead of one tangled snapshot.

Lecture 6: Packaging and Shipping Code

Difficulty: Medium · MIT IAP 2026 · Software Delivery · Key topics: pip/uv & virtual environments, dependency resolution, semantic versioning, lock files, Docker images & containers, configuration & secrets

Frames shipping code as producing an artifact — a packaged, distributable output distinct from source code — plus a clear statement of what environment that artifact assumes, illustrated throughout with Python’s pip/uv, wheels, and virtual environments. Covers dependency hell (two packages requiring incompatible version ranges of the same transitive dependency) and its fix, per-project virtual environments that isolate installed packages and can even pin a specific language runtime version. Introduces Semantic Versioning’s MAJOR.MINOR.PATCH convention for communicating what kind of change a release contains, contrasts it with date-based Calendar Versioning, and explains why libraries should specify loose version ranges while applications should pin exact versions and commit a lock file for reproducibility. Covers containers as a lighter-weight alternative to full VMs — sharing the host’s kernel rather than virtualizing an entire machine — walking through a real Dockerfile, its layer-caching behavior, and common mistakes like unpinned versions and unnecessary layers, and closes by distinguishing configuration (environment variables, config files, secrets) that should vary per-deployment from code that should not.

Key Points:

  • An artifact (e.g. a Python wheel) is the packaged, distributable output built from source code; packaging solves the problem that code run from a different directory can’t find its own modules without being properly installed.
  • Dependency hell happens when two required packages need incompatible version ranges of the same transitive dependency; virtual environments (python -m venv or uv venv) isolate a project’s installed packages and can pin a specific Python version.
  • uv pip install follows the same interface as plain pip install but is dramatically faster; uv lock produces a lock file pinning every resolved package to an exact version and hash.
  • Semantic Versioning (MAJOR.MINOR.PATCH): PATCH means backwards-compatible bug fixes only, MINOR adds backwards-compatible features, MAJOR signals breaking changes; libraries should specify loose ranges, applications should pin exact versions.
  • A Docker image is the packaged artifact (built in cached layers from a Dockerfile’s FROM/RUN/COPY instructions); a container is a running instance of that image, and multiple containers can run from one image.
  • Common Dockerfile mistakes: using the full image instead of a -slim variant, splitting installs across many RUN layers instead of one, leaving versions unpinned, not cleaning package-manager caches, and running as root.
  • Configuration (environment variables, YAML/config files) should let the same codebase deploy to development, staging, and production with no code changes; secrets like API keys must never be hardcoded or committed to version control.

Practice Tip: Build the lecture’s own two-stage Dockerfile improvement yourself — start from the naive version (full python:3.12 image, unpinned versions, separate RUN lines), then rewrite it using python:3.12-slim, a single combined apt-get install with cache cleanup, and a pinned uv sync --locked — and compare the resulting image sizes with docker images to see the layers and mistakes cost real megabytes, not just style points.

Common Mistake: Pinning exact dependency versions (e.g. requests==2.32.3) in a published library the way you would in an application — this is backwards from the lecture’s own guidance: libraries should specify loose, compatible ranges so they don’t create version conflicts for whoever installs them alongside their other dependencies, while pinning exact versions (with a lock file) is the right call for an application or service, which is the final consumer of its dependencies and needs reproducibility instead of compatibility.

Important Questions:

  • Why does the lecture recommend loose version ranges for libraries but exact pinned versions for applications? A library is meant to be imported alongside other code that has its own dependencies, so an overly strict version pin can create conflicts with a user’s other packages requiring a different version of that same shared dependency (dependency hell); an application or service is the final consumer of its dependency tree and exposes only a user interface or API rather than being imported by other code, so pinning exact versions in a lock file (e.g. via uv lock) is the right call there, guaranteeing everyone running the application gets the exact same reproducible set of dependencies.
  • What is the practical difference between a Docker image and a Docker container, and why does that distinction matter for the builder pattern? A Docker image is the packaged, static artifact built from a Dockerfile’s layered instructions — a template — while a container is a running instance of that image, and many containers can be started from the same image; the builder pattern exploits this by copying just a prebuilt binary (like the uv binary from a published image) into another image, so the final image ships only the tool actually needed to run the application rather than the entire toolchain used to build it.

Lecture 7: Agentic Coding

Difficulty: Medium · MIT IAP 2026 · AI-Assisted Development · Key topics: LLM inference & context windows, tool-calling agents, test-driven prompting, MCP, parallel agents & git worktrees, AI coding privacy & limitations

Defines coding agents as conversational AI models with access to tools — reading/writing files, running shell commands, web search — living either inside an IDE or as standalone command-line/GUI harnesses, and builds directly on the autocomplete-and-inline-chat material from the Development Environment lecture. Explains the underlying mechanism at a high level: an LLM models a probability distribution over completion strings given a prompt string within a fixed context window, and a tool-calling agent’s harness repeatedly re-supplies the full conversation plus each tool call’s result back into the model, running inference again every time a tool is invoked. Surveys concrete use cases — implementing features (most effective paired with test-driven development, where the agent implements against tests you’ve already reviewed), fixing errors by giving the model a feedback loop it can run itself, refactoring, and code review — while flagging that most cloud-based AI coding tools send your code to a third-party service, a real privacy consideration. Closes with advanced patterns (reusable prompts/skills, running parallel agents on isolated git worktrees, MCP servers for connecting agents to external tools) and a clear list of failure modes: LLMs can produce plausible-looking wrong code, can spiral down unproductive debugging rabbit holes, and are not a substitute for actually understanding the code they generate.

Key Points:

  • A coding agent differs from plain autocomplete or inline chat because it can invoke tools (read/write files, run shell commands, web search) across multiple turns, not just suggest or edit text once.
  • LLM inference samples from a probability distribution over completion strings given a prompt string, within a fixed context window; a tool-calling agent re-runs inference every time a tool call returns a result, feeding that result back in as part of the prompt.
  • Coding agents work best in a feedback loop — give the agent a way to run the failing check itself (a type checker, a test suite) so it can iterate autonomously instead of you manually reporting each new error.
  • Test-driven prompting (write and review tests first, then have the agent implement against them) is called out as a particularly effective way to give an agent a clear, checkable specification.
  • Most AI coding tools in their default configuration send your code and conversation history to a cloud service; open-source local models exist but are currently infeasible for most users’ hardware.
  • git worktrees let multiple coding agents work on different tasks (or multiple attempts at the same task) in parallel without their file changes interfering with each other.
  • MCP (Model Context Protocol) is an open protocol connecting a coding agent to external tools and data sources, such as a server that lets an agent read and write documents in a third-party app.

Practice Tip: Take a small script with two or three functions and prompt an agent to convert it to a proper CLI with argument parsing, type annotations, and a passing type check; watching it read the file, edit it, then actually invoke the type checker and iterate on any failure is the fastest way to see the “feedback loop” idea in action rather than read about it.

Common Mistake: Treating an agent’s confident, well-formatted answer as automatically correct — the lecture is explicit that LLMs can produce plausible-sounding but wrong code and can lead you down unproductive debugging spirals; verifying agent output (ideally against a real test or type check the agent can also run) is not optional, especially since the lecture also notes that verifying some code can be harder than writing it yourself.

Important Questions:

  • Why does giving a coding agent a feedback loop (like a failing test or type checker it can run itself) make it more effective than just describing the problem in one prompt? A tool-calling agent’s harness re-runs LLM inference every time a tool call returns a result, so if the agent can invoke the failing check directly (e.g. a type checker or a test suite) it can see the actual error output and iterate on its own fix without you manually copying error messages back into the conversation each time; this turns a single request into an autonomous correct-and-recheck loop, which the lecture identifies as where coding agents are most effective.
  • What privacy tradeoff does the lecture identify with most default AI coding tool configurations? In their standard configuration, most AI coding tools send a substantial amount of your data to the cloud — sometimes just the LLM inference runs remotely while the harness runs locally, but in other setups even more of the software runs in the cloud, meaning the service provider can effectively receive a full copy of your repository plus every interaction you have with the tool; open-source local alternatives exist but running bleeding-edge open models locally is currently infeasible for most users due to hardware requirements.

Lecture 8: Beyond the Code

Difficulty: Easy · MIT IAP 2026 · Engineering Communication · Key topics: code comments, README structure, commit messages, bug reports, code review, asking good questions, AI disclosure & etiquette

Argues that good engineering writing captures the why, not the what, since the what is usually self-evident from the code itself while the reasoning behind it is easily lost; applies this to seven categories of genuinely worthwhile code comments (TODOs with real context, references to external sources, correctness arguments, hard-won debugging lessons, rationale for magic-number constants, load-bearing implementation choices, and explicit “why not” notes on rejected approaches). Covers README structure as a funnel — what it does, why care, how to use it, how to install it, in that order, showing usage before installation — and commit messages as the historical record future readers rely on, recommending a Problem → Solution → Implications structure for anything beyond a trivial change, plus committing logically separate changes individually rather than mixing refactors with features. Turns to collaboration: writing a good bug report (environment, expected vs. actual, specific reproduction steps, what you’ve already tried) and reviewing code well — reviewing the code rather than the person, giving actionable rather than vague feedback, asking questions instead of issuing demands. Closes with asking good questions effectively (stating your current understanding first, asking yes/no questions, being specific) and AI etiquette — disclosing meaningful AI contribution, following a team’s own AI norms, and never using AI where an assessment explicitly forbids it.

Key Points:

  • A good code comment explains why, not how — the code itself already shows how; a useless TODO says “optimize”, a useful one says why the shortcut is fine now and what will force it to change.
  • A README should answer, in order: what does this do, why should I care, how do I use it, how do I install it — showing usage before installation so a reader can judge relevance in seconds.
  • A commit message’s body should answer what problem forced the change, what alternatives were considered, what the trade-offs are, and what might be surprising about the approach — not just restate the what in different words.
  • Complex changes are better structured as Problem → Solution → Implications, and split into logically separate commits (git add -p) rather than mixed together, which also enables git bisect later.
  • A good bug report includes environment details, expected vs. actual behavior, specific reproduction steps, and what’s already been tried — respecting that maintainer time is scarce and oversubscribed.
  • Good code review critiques the code, not the person, prefers actionable comments over vague demands, asks questions rather than issuing orders, and distinguishes blocking issues from mere suggestions.
  • Asking good questions means stating your current understanding first (“is that right?”), asking yes/no questions where possible, being specific, and not accepting an incomplete answer just to move on.

Memory Tip: Anchor every kind of writing this lecture covers — comments, commit messages, bug reports, review comments — to the same one-word test: does this capture the why? A comment, commit, or review note that only restates the what the diff already shows has failed that test, no matter how detailed it is.

Common Mistake: Writing a commit message or code comment that only restates what the diff already shows (e.g. “fixed bug”, or a comment that paraphrases the very next line of code) — the lecture is explicit that the what is usually self-explanatory from the code itself, so a comment or commit message earns its place only by capturing the why: the problem that forced the change, the alternatives considered, or a non-obvious reason the chosen approach is correct.

Important Questions:

  • According to this lecture, what distinguishes a genuinely useful TODO comment from a useless one? A useless TODO like “TODO: optimize” gives a future reader no context to act on; a useful one, such as one that names the specific input size a shortcut is fine for and what would force it to change, explains both what is incomplete and why it was deliberately deferred, so someone revisiting the code later — including the original author — can judge whether the deferred work still matters without re-deriving the reasoning from scratch.
  • Why does the lecture recommend structuring a commit message’s body around what problem forced the change, rather than just describing what changed? Because the “what” is already visible in the diff itself, restating it in the commit message adds no information; the historical-record value of a commit message comes from capturing what a git blame reader cannot see in the code alone — the forcing problem, the alternatives that were considered and rejected, the trade-offs accepted, and anything surprising about the chosen approach — which is exactly the reasoning a future developer needs before they change that code again.

Lecture 9: Code Quality

Difficulty: Medium · MIT IAP 2026 · Code Quality & CI · Key topics: formatters, linters (Ruff, semgrep), unit/integration/property-based testing, code coverage, pre-commit hooks, CI/CD, command runners, regular expressions

Surveys the tooling that supports writing high-quality code: auto-formatters (Prettier, Black, gofmt) that standardize surface syntax like quote style and import order so developers stop bikeshedding it, and linters (language-specific tools plus AST-level tools like semgrep) that run static analysis to catch deeper antipatterns, with some rules auto-fixable and others requiring a documented reason to suppress. Covers testing at multiple granularities — unit, integration, and functional tests, test-driven development, regression tests written after a bug is found, and property-based testing (pioneered by Haskell’s QuickCheck, implemented in Python’s Hypothesis) — plus code coverage as a useful but imperfect metric for gauging test completeness. Explains how pre-commit hooks run formatters/linters/tests automatically before every commit, and how continuous integration services like GitHub Actions run the same checks in check-only (not auto-fix) mode on every push or pull request, often extended into continuous deployment; introduces command runners like just so a team doesn’t have to memorize long invocations. Closes with regular expressions as a cross-cutting pattern-matching language — literal characters, character classes, quantifiers, anchors, and capture groups — noting that regexes are powerful but not all-powerful, since some languages (like matched-nesting or HTML) are provably not regular.

Key Points:

  • Formatters like Prettier, Black, and gofmt standardize surface syntax (quote style, spacing, import order) automatically, ending style debates; their configuration file should be checked into version control.
  • Linters run static analysis to catch deeper antipatterns than formatting; semgrep works at the AST level across languages and lets you write custom rules, e.g. flagging dangerous subprocess.Popen(..., shell=True) calls.
  • Tests span different granularities (unit, integration, functional); property-based testing (e.g. Python’s Hypothesis) generates many inputs automatically to check a property holds, rather than checking one hand-picked example at a time.
  • Code coverage shows which lines actually run during tests, useful for finding untested paths, but is an imperfect metric — high coverage doesn’t guarantee high-quality tests.
  • Pre-commit hooks run formatters, linters, and sometimes tests automatically before every git commit; CI services like GitHub Actions run the same tools in check-only mode on every push or pull request.
  • Command runners like just turn a long invocation such as uv run ruff check --fix into a short, memorable one like just lint, standardizing how a team runs its tooling.
  • Regex building blocks: . (any character), [abc]/[^abc] (character classes), a|b (alternation), \d/\w/\b (digit/word/word-boundary), ?/*/+/{N} (quantifiers), (...) (capture groups), ^/$ (anchors).

Practice Tip: Write a regex to extract the requested path from a real web-server log line (e.g. an Nginx access log entry), test it against several sample lines, then wrap it in a capture group and extract just the path with your language’s regex library — moving from “the pattern matches” to “I can pull out exactly the field I need” is where regex actually becomes useful day to day.

Common Mistake: Treating a high code-coverage percentage as proof the tests are good — the lecture is explicit that coverage is a metric, not a goal in itself: a test suite can execute every line (100% coverage) while asserting nothing meaningful about correctness, so coverage should guide you toward untested paths, not substitute for actually writing tests that check the right properties.

Important Questions:

  • What is the practical difference between what a formatter checks and what a linter checks? A formatter like Prettier or Black only rewrites surface syntax — things like quote style, spacing around operators, and import ordering — without reasoning about what the code does; a linter goes deeper, running static analysis to catch antipatterns and potential bugs (such as an unnecessarily nested if-statement, or a dangerous shell=True subprocess call), and some linter findings can be auto-fixed while others require a human decision or an explicit per-line suppression.
  • Why does the lecture say CI should generally run tools in “check-only” mode rather than “fix” mode? CI’s job is to verify that code pushed to the repository already meets the project’s standards; if the CI script silently auto-fixed formatting or lint issues, it would hide the fact that a contributor’s code didn’t actually comply, so the tool instead raises an error when code is non-compliant, making the failure visible and attributable to the specific commit or pull request that introduced it rather than papered over automatically.

Read The Missing Semester of Your CS Education for Free

This is a genuinely free, official MIT course — there is nothing to buy and no PDF to search for. Every lecture’s full written notes, exercises, and recorded video are hosted directly by MIT on the course’s own site, licensed CC BY-NC-SA 4.0 so you’re free to read, adapt, and share them (non-commercially, with attribution) without asking permission. The button below opens the official 2026 lecture index.

→ Read Online

How to Study This Book

The 9 lectures are designed to be largely independent, but the 2026 syllabus still builds gently as it goes, so working through them in the published order — Course Overview + The Shell, Command-line Environment, Development Environment and Tools, Debugging and Profiling, Version Control and Git, Packaging and Shipping Code, Agentic Coding, Beyond the Code, and Code Quality — is the safest path, especially for the first two lectures: Lecture 2’s streams, redirection, and environment-variable material assumes Lecture 1’s shell navigation and core utilities are already comfortable.

Lectures 1 and 2 together are the real foundation of the course — almost every later lecture assumes you can already navigate a shell, chain a pipeline, and redirect output without thinking about it. If you’ve never used a terminal seriously before, budget real time here rather than skimming toward the more eye-catching later lectures.

Lecture 3 (Development Environment and Tools) can be studied any time once the shell feels comfortable. Its own advice is the most useful study tip in the whole course: enable Vim mode in every tool you use that supports it and actually use it for real editing for a month, rather than reading the movement and editing commands as reference material to look up later.

Lecture 4 (Debugging and Profiling) is the densest and most tool-heavy lecture in the course — gdb, rr, strace, eBPF, sanitizers, perf, and flame graphs in one page. It rewards running the lecture’s own worked C examples yourself (a use-after-free bug for AddressSanitizer, a corrupted-struct bug for rr, a slow nested loop for perf) rather than reading the commands passively.

Lecture 5 (Version Control and Git) is written bottom-up on purpose: read the data-model section (blobs, trees, commits, references) slowly and in order before jumping to the command-line reference table at the end, since the whole point of the lecture is that the commands make sense once the underlying object graph does, not the other way around.

Lecture 6 (Packaging and Shipping Code) leans on Python (pip/uv, virtual environments) and Docker as its running examples throughout. If you don’t already have Python and Docker installed, expect this lecture to need more environment setup before you can follow along than any of the others.

Lectures 7 and 8 (Agentic Coding and Beyond the Code) are the newest material in the 2026 offering. Agentic Coding explicitly builds on the AI-powered development subsection inside Lecture 3, so read that subsection first if you jump straight to Lecture 7; Beyond the Code has no coding prerequisite at all and can be read independently, including by anyone who has never taken this course’s earlier lectures.

Lecture 9 (Code Quality) closes the course and is the one most worth revisiting once you have a real personal or class project to actually wire a formatter, linter, pre-commit hook, and CI pipeline into — reading it in the abstract, with nothing to apply it to, is the least effective way to use it.


Used In These Programs

This resource is used as a practical skills supplement — not a graded course in itself — alongside BS Computer Science and BS Information Technology coursework, for students who want real fluency with the shell, editors, Git, and modern development tooling that a formal curriculum rarely covers directly. Browse all Programming books or all Computer Science books.

Who Should Read This

The Missing Semester of Your CS Education is written for anyone past their very first programming course who wants to stop fighting their own tools — BSCS and BSIT students working on group projects, students about to start an internship or their first real codebase, and self-taught programmers who learned a language but never learned the shell, Git, or a real editor properly. It assumes basic prior programming exposure but deliberately teaches no algorithms, data structures, or CS theory of its own, so it pairs naturally alongside — not instead of — a formal Data Structures or Discrete Math textbook such as Data Structures in C++ (Aikman Series). It is not the right choice if you are looking for a graded textbook with exams and a fixed syllabus, or for a deep dive into any one of its 9 topics — each lecture is a focused, practical introduction meant to be extended with real use, not the last word on Git internals or debugging theory.


Applicable Universities

This resource is useful for students at Pakistani universities offering BSCS, BSIT, or BSSE programs, including Punjab University, Virtual University, COMSATS, FAST, UET, NUST, GIKI, LUMS, and other HEC-recognized institutions, and for any self-taught programmer who wants the practical tooling skills a formal degree program rarely teaches directly.

FAQs

Is The Missing Semester of Your CS Education free?

Yes. Every lecture’s written notes, exercises, and recorded video are hosted directly by MIT on the course’s own official site under the CC BY-NC-SA 4.0 licence, which lets you share and adapt the material for free as long as you give credit, don’t use it commercially, and license any derivative work the same way. There is no PDF, no paywall, and no sign-up required to read any lecture.

Who teaches this course, and is it an official for-credit MIT class?

The course is co-taught by Anish Athalye, Jon Gjengset, and Jose Javier Gonzalez Ortiz, all MIT alumni who started teaching it as students themselves. It runs as a not-for-credit class during MIT’s “Independent Activities Period” (IAP) each January — the lectures themselves are only open to the MIT community, but all lecture notes and video recordings are made freely available to the public afterward.

How many lectures does the 2026 course have, and how is it organized?

The 2026 offering has 9 one-hour lectures: Course Overview + Introduction to the Shell, Command-line Environment, Development Environment and Tools, Debugging and Profiling, Version Control and Git, Packaging and Shipping Code, Agentic Coding, Beyond the Code, and Code Quality. Each lecture is largely independent, though later lectures do lean on tooling fluency built in the first two.

Is it good for complete beginners to computer science?

It assumes you have already taken at least one programming course and are comfortable writing simple programs, but it deliberately teaches no CS theory, algorithms, or data structures of its own — it is a tooling and workflow course, not an introduction to programming. Someone who has never written any code before should start with an introductory programming book first, then come back to this course.

Does this course cover Git and version control in real depth?

Yes — the whole of Lecture 5 is dedicated to Git, and it is taught unusually well: rather than listing commands to memorize, it explains Git’s underlying data model (snapshots, content-addressed objects, references) first, then shows how the command-line interface is just a set of operations on that model, which is why the lecture describes most other Git tutorials’ command-first approach as leading to memorized “magic incantations” instead of real understanding.

Is the 2026 edition still current, and does it cover AI coding tools?

Yes to both. The course is substantially revised every year it runs, and the 2026 offering is the current, actively maintained edition — some special topics from the 2019 and 2020 offerings (Security and Cryptography, Data Wrangling, Automation, and others) were dropped from the current 9-lecture syllabus but remain available as archived “special topics from previous years.” Rather than one standalone AI lecture, the course folds AI-tool usage directly into the lectures where it’s relevant — AI-powered autocomplete and inline chat in the Development Environment lecture, and a full dedicated Agentic Coding lecture covering coding agents, MCP, and AI etiquette.

Do I need to watch the video lectures, or are the written notes enough?

The written lecture notes on the official site are self-contained and are what this page and its chapter summaries are based on; the course itself notes that some material covered as live demos in the video recordings may not appear in the written notes, so the videos are a useful supplement for depth but are not required to follow along with each lecture’s core material and exercises.

Related Books

The Missing Semester of Your CS Education, taught by Anish Athalye, Jon Gjengset, and Jose Javier Gonzalez Ortiz at MIT, is a free 9-lecture course covering the practical tooling a formal CS curriculum rarely teaches directly — the shell, a real text editor, debugging and profiling, Git, packaging and shipping code, AI coding agents, engineering communication, and code quality tooling. Browse more Computer Science books for the rest of your semester.

The Missing Semester of Your CS Education, by Anish Athalye, Jon Gjengset, and Jose Javier Gonzalez Ortiz. An MIT Independent Activities Period (IAP) course, 2026 edition. All content — including the website source, lecture notes, exercises, and lecture videos — is licensed under CC BY-NC-SA 4.0. Read it free at its official source, https://missing.csail.mit.edu

Leave a Comment