Git & Debugging
I spent the first two years of my engineering career fixing bugs by guessing. I would change something, run the code, and see if the error changed. Sometimes it worked. Mostly it taught me that random changes produce random outcomes. The moment I started reading tracebacks literally — treating every line as a clue rather than noise — my debugging speed changed completely. The traceback tells you the file, the line, and what went wrong. The skill is knowing how to read it.
This lesson sets up the environment every subsequent module uses, builds the mental model behind Git, covers the workflow that runs through all capstone projects, and walks through the three error types you will hit most often in Python. If your environment is already set up, skip to the Git section.
Environment setup
Skip if you already have Python 3.12, uv, and VS Code installed
Python 3.12 via uv
uv is a fast Python package and project manager. It handles both Python installations and virtual environments in one tool.
Install uv:
curl -LsSf https://astral.sh/uv/install.sh | sh
Then install Python 3.12:
uv python install 3.12
Verify:
uv run python --version
You should see Python 3.12.x.
VS Code
Download from code.visualstudio.com. Install the Python extension (Microsoft) and the Ruff extension for linting. No other extensions are needed for this curriculum.
Verify everything works
In a new directory, create a project and run a file:
mkdir shipml && cd shipml
uv init
uv run python -c "import sys; print(sys.version)"
If you see the Python version printed, the environment is ready.
How Git thinks
Most Git confusion comes from using commands before having a model of what Git is actually doing. Git does not store diffs — it stores snapshots. Every commit is a complete picture of the project at a moment in time. A branch is not a folder or a copy; it is a label that points to one commit. HEAD is another label — it points to wherever you are right now. That is the whole model. Scroll through the diagram to see it built up piece by piece.
A commit is a snapshot
When you run git commit, Git saves a complete snapshot of every tracked file. Each commit gets a unique hash — a short identifier like a1b2c3. The hash is computed from the content, so two identical snapshots always produce the same hash.
The first commit has no parent. Every commit after it points back to the one before it, forming a chain.
A branch is just a label
A branch is not a copy of the code. It is a label — a pointer to one specific commit. When you create a new branch, Git writes a file with one line: the hash of the current commit. Nothing is duplicated.
main points to the latest commit on the main line. That is all it is.
HEAD tracks where you are
HEAD is Git's way of knowing which commit you are working on. Usually HEAD points to a branch label, which in turn points to a commit. When you switch branches with git checkout, Git moves HEAD to that branch label and updates your files to match that commit's snapshot.
HEAD moves forward automatically every time you commit — it always stays on the tip of your current branch.
Creating a feature branch
When you run git checkout -b feature/my-change, Git creates a new label pointing at the same commit as HEAD — then moves HEAD to the new label. The two branches share history up to this point; no files are copied.
Commits you make now advance feature/my-change while main stays where it was.
Diverging history
After a few commits on feature/my-change, someone else lands a commit on main. The two branches now have different histories from the same ancestor. This is completely normal — it is the point of branches.
Git records both lines independently. Neither branch knows about the other's commits yet.
Merging — and conflicts
When you merge feature/my-change into main, Git finds the common ancestor and applies both sets of changes. If the two branches changed different lines, Git combines them automatically. If they changed the same lines, Git cannot decide which is correct — it marks the conflict and asks you to resolve it.
A conflict is not a failure. It is Git saying: two engineers touched the same line; a human needs to pick the right version.
With that model in place, the Git commands have something to attach to. git checkout -b moves the HEAD label to a new branch pointer. git commit advances the branch pointer to a new snapshot. git merge creates a new commit with two parents.
Git branches and pull requests
Every capstone project in this curriculum has a GitHub repository. The workflow is the same across all of them: feature work happens on a branch, not on main. This keeps main deployable and lets you get feedback before merging.
Creating a branch
git checkout -b feature/my-change
This creates a new branch label at the current commit and moves HEAD to it. All commits from this point advance feature/my-change, not main.
Committing and pushing
git add path/to/changed-file.py
git commit -m "describe what this commit does and why"
git push origin feature/my-change
Commit messages describe the change and its reason — not the implementation. “fix bug” tells the reader nothing. “fix KeyError when config dict is missing model key” tells them exactly what broke and where.
Opening a pull request
After pushing, GitHub shows a prompt to open a pull request. A pull request is a record of what you changed and why. In a capstone, the PR description is where you explain the design decision: why this approach, what you considered and rejected, what you would do differently given more time.
Merging
When the PR is approved (or self-reviewed), merge it on GitHub. Then pull the updated main locally:
git checkout main
git pull origin main
The branch can be deleted after merging — you do not need it anymore.
Conflicts
A merge conflict happens when two branches change the same lines. Git marks the conflict in the file with <<<<<<<, =======, and >>>>>>> markers. The top section (between <<<<<<< and =======) is your change. The bottom section (between ======= and >>>>>>>) is what main has. Edit the file to keep what is correct — sometimes one side, sometimes a combination — then remove the markers, stage the file, and complete the merge:
git add path/to/resolved-file.py
git merge --continue
Reading tracebacks
A traceback is Python’s record of what happened and where. It prints from the outermost call to the innermost failure. The error type and message are the last two lines — they are almost always the most useful part.
IndexError — accessing an index that does not exist
results = [] print(get_first_score(results))
def get_first_score(scores):
return scores[0]
results = []
print(get_first_score(results))
The traceback shows the call chain: get_first_score(results) was called with an empty list, and scores[0] failed because there is no element at index 0. The fix is to check whether the list is empty before indexing, or to handle the exception at the call site. Reading IndexError: list index out of range is enough to know: something tried to access an element that does not exist.
KeyError — accessing a dictionary key that is not there
config = {"temperature": 0.7, "max_tokens": 512}
print(config["model"])KeyError: 'model' tells you exactly which key was missing. The config dictionary had temperature and max_tokens but not model. The fix depends on intent: use config.get("model") if the key is optional and None is an acceptable value, or add model to the config if it is required.
AttributeError — calling a method on None
result = None print(get_upper(result))
def get_upper(value):
return value.upper()
result = None
print(get_upper(result))
AttributeError: 'NoneType' object has no attribute 'upper' means value was None when .upper() was called. The traceback points to return value.upper() in get_upper. The bug is not in get_upper — it is wherever None was passed in. Following the call chain up the traceback locates the source: get_upper(result) was called with result = None.
The pattern across all three: read the last two lines first (error type and message), then read the traceback bottom-up to find where the bad value came from.
The next lesson covers how projects and capstones work — what to submit, where to find the starter repos, and how the rubric is applied.