Skip to main content

Version control with Git and GitHub

You now have Python on your machine and a real program you can run. There's one tool sitting between "I wrote a program" and "I have a project I can save, undo, and show the world" — and almost every guide above this one quietly assumes you already use it. That tool is Git, and its most popular home is GitHub. This lesson teaches both from zero. It's a how-to with traced examples rather than a coding exercise, so there's no in-browser challenge — Git is a terminal tool, so we'll show you the commands and example output, and you'll follow along on your own machine. By the end you'll save your small hello.py project in Git and GitHub. You'll use the same workflow when you build the capstone next.

The problem Git solves (in everyday words)​

Imagine you're writing an essay. You save essay.docx. You make changes, but you're scared to lose the old version, so you save essay_v2.docx. Then essay_final.docx. Then essay_final_FINAL.docx. Then essay_final_FINAL_v2_actually_done.docx. Now you have eight files, no idea which is newest, and no way to see what changed between any two of them. If you delete the wrong one, it's gone.

That's exactly the mess programmers were in before version control. Version control is software that takes care of all of this for you: it keeps a complete history of your project, lets you save labelled snapshots whenever you want, see precisely what changed between any two of them, and rewind to any earlier point — all inside one folder, with no _FINAL_v2 filenames ever again. Git is the version-control tool nearly the whole industry uses. (There are others, but learning one is learning the idea, and Git is the one jobs ask for.)

So Git answers three questions you'll ask constantly:

  • What did my project look like last Tuesday? (history)
  • What exactly did I change since then? (a precise diff, line by line)
  • Can I undo this and go back? (yes, to any saved snapshot)

The mental model: snapshots, commits, and the repo​

The single most important idea in Git is the commit. A commit is a saved snapshot of your entire project at one moment, with a short message you write describing what changed (e.g. "Add letter-grade function"). Think of it as a labelled photograph of every file, frozen forever. Your project's history is just a long chain of these photographs, newest at the end.

The folder Git is tracking — your project folder, now with a hidden history attached — is called a repository, or repo for short. When you turn a normal folder into a repo, Git creates a hidden sub-folder named .git inside it. That .git folder is the history: every commit, every old version, all stored there. You almost never touch it directly; Git commands read and write it for you. Delete .git and you delete the history (the current files stay, but the time machine is gone).

The three areas: working directory → staging → commit​

Here's the one piece of Git that confuses every beginner, explained slowly. When you change a file, it doesn't go straight into a commit. It passes through three areas:

  1. The working directory — your actual files on disk, the ones you edit. When you save hello.py in your editor, the change lives here. Git sees it but hasn't recorded anything yet.
  2. The staging area (also called the index) — a holding pen for "changes I want in my next commit." You explicitly move changes here with git add. This lets you choose exactly what goes into a snapshot — maybe you changed three files but only want to commit two of them.
  3. The repository (committed history) — when you run git commit, everything currently in the staging area becomes a new permanent snapshot in the .git history.

Read it as a one-way pipeline: you edit (working directory) → you stage the changes you want (git add) → you snapshot them (git commit).

working directory staging area repository
(your files on disk) ("next commit") (.git history)
───────────────── ───────────── ───────────────
edit hello.py ──git add──▶ hello.py ──git commit──▶ 📸 snapshot #3
📸 snapshot #2
📸 snapshot #1

Why two steps (stage, then commit) instead of one? Because the staging area lets you craft a clean snapshot — group related changes into one meaningful commit and leave unrelated half-finished work for later. It feels like extra typing at first; it becomes second nature fast.

The core loop: init, status, add, commit, log​

Let's actually do it. We'll make a tiny project and walk every command, showing the real output. First, a one-time setup so Git knows who you are (it stamps your name and email on every commit). Run these once on your machine, ever:

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

Now create a project folder and turn it into a repo. git init is the command that creates the .git history folder, turning a plain folder into a Git repository:

mkdir hello-git
cd hello-git
git init

Git replies:

Initialized empty Git repository in /Users/you/hello-git/.git/

That's the repo, born. Now create a file (use your editor, or this quick terminal trick), then ask Git what it sees with git status — the command you'll run more than any other. It reports the state of your three areas:

echo "print('hello git')" > hello.py
git status
On branch main

No commits yet

Untracked files:
(use "git add <file>..." to include in what will be committed)
hello.py

nothing added to commit but untracked files present (use "git add" to commit)

Read that carefully — Git is being genuinely helpful. Untracked means "this file exists in your working directory but Git isn't watching it yet." Git even tells you the next command. Let's stage the file with git add (move it into the staging area), then check status again:

git add hello.py
git status
On branch main

No commits yet

Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: hello.py

Now hello.py is staged — sitting in the "next commit" holding pen. Take the snapshot with git commit. The -m flag (short for message) attaches your description:

git commit -m "Add hello.py"
[main (root-commit) a1b2c3d] Add hello.py
1 file changed, 1 insertion(+)
create mode 100644 hello.py

That a1b2c3d is the start of the commit's unique ID (a long fingerprint Git shortens for display). You just saved your first snapshot. See the history with git log:

git log
commit a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 (HEAD -> main)
Author: Your Name <you@example.com>
Date: Tue Jun 24 10:15:02 2026 -0700

Add hello.py

(Press q to exit the log view if it fills the screen.) Now do it again — the everyday loop. Edit the file, and watch how status changes for a file Git is already tracking:

echo "print('goodbye git')" >> hello.py
git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: hello.py

no changes added to commit (use "git add" to commit)

This time it says modified (not untracked) — Git knows this file and sees it changed. The loop is identical: stage, then commit.

git add hello.py
git commit -m "Add a goodbye line"
[main f9e8d7c] Add a goodbye line
1 file changed, 1 insertion(+)

That's the entire core loop, and you'll repeat it for the rest of your life as a programmer: edit → git add → git commit -m "...", checking git status whenever you're unsure where things stand. Five commands — init, status, add, commit, log — cover the vast majority of daily Git.

:::tip Write commit messages for future-you A good message says what changed and why, in the imperative ("Add empty-list guard to average", not "stuff" or "fixed it"). Six months from now, git log is the only record of why you did something. Be kind to future-you. :::

Connecting to GitHub: remote, push, and what GitHub adds​

Everything so far lived only on your computer. Git is happy entirely offline — the whole history is in your local .git folder. So what's GitHub? GitHub is a website that hosts Git repositories online. Git is the tool; GitHub is a place to put a copy of your repo on the internet. (GitLab and Bitbucket are similar sites — same Git underneath.)

GitHub adds things a local repo can't:

  • A backup — your code survives even if your laptop dies.
  • A shareable link — send someone a URL instead of zipping files.
  • A portfolio — employers look at GitHub profiles; a repo is proof you built something.
  • Collaboration — others can copy, suggest changes, and contribute.

The copy of your repo that lives on GitHub is called a remote — a version of the repo hosted somewhere other than your machine. The default remote is conventionally named origin. Sending your local commits up to the remote is called a push; pulling new commits down is a pull.

Here's the full path from local repo to GitHub, traced. First, on the GitHub website, create a new empty repository (click New, give it a name like hello-git, and — important — do not let it add a README or .gitignore for you yet, so it stays empty). GitHub then shows you a URL like https://github.com/yourname/hello-git.git. Back in your terminal, in your project folder:

git remote add origin https://github.com/yourname/hello-git.git
git branch -M main
git push -u origin main

Line by line:

  • git remote add origin <url> tells your local repo "there's a remote, call it origin, it lives at this URL." Nothing is sent yet — you've just saved the address.
  • git branch -M main makes sure your branch is named main (a branch is a line of development; you've been on the default one all along — for now, just know that main is the standard name for the primary branch).
  • git push -u origin main uploads your commits to origin. The -u sets origin/main as the default destination, so future pushes are just git push.

The push prints something like:

Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Writing objects: 100% (6/6), 512 bytes | 512.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To https://github.com/yourname/hello-git.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.

Refresh the GitHub page and your files are there, with your commit history. From now on the loop gains one final step: edit → git add → git commit -m "..." → git push. Commit often locally; push when you want the online copy updated.

:::note GitHub will ask you to prove it's you The first push asks you to authenticate. GitHub no longer accepts your account password on the command line. The simplest beginner path is a personal access token (PAT): on GitHub, go to Settings → Developer settings → Personal access tokens, generate one, and paste it when the terminal asks for a password. Or install the official GitHub CLI (gh) and run gh auth login, which handles it for you. This is a one-time setup per machine. :::

.gitignore: keep junk and secrets out of your repo​

Some files should never go into a commit. Two big categories:

  • Generated / bulky junk — your .venv folder (hundreds of installed-package files you can recreate anytime with pip install -r requirements.txt), Python's __pycache__ cache folders, build output. Committing these bloats the repo with stuff anyone can regenerate.
  • Secrets — API keys, passwords, tokens, a .env file holding credentials. This is the serious one. Anything you commit and push to a public GitHub repo is visible to the entire internet — deleting the file later does not remove it from earlier commits. Pushing a secret key is one of the most common and most damaging beginner mistakes.

A useful safeguard is .gitignore — a plain text file, one pattern per line, telling Git which untracked files to leave out of normal git add operations. It does not stop tracking files you have already added, and git add --force can override it. It helps prevent mistakes, but it is not a security boundary. A solid starter .gitignore for a Python project:

# Virtual environment — recreate it with pip, never commit it
.venv/

# Python bytecode cache
__pycache__/
*.pyc

# Secrets — API keys, passwords, tokens. NEVER commit these.
.env
secrets.json

Put this file in your project folder before your first git add. With an untracked .venv present and ignored, git status won't list it as untracked. An already tracked file still appears when changed, even if you later add its name to .gitignore.

Before each commit, inspect both git status and git diff --staged. A diff shows changed lines; --staged limits it to the changes queued for the next commit. Lines starting with + are additions and lines starting with - are removals. Read those changes for secrets or files you did not intend to share. If you find one, stop and remove it from the staged changes before committing; adding an ignore pattern alone does not unstage it.

:::warning If you've already committed a secret, rotate it Removing a leaked key from your files and committing the removal is not enough — it's still sitting in the history, readable by anyone with the repo. Revoke/rotate the key at the provider (disable the old one and generate a replacement) and treat the old one as compromised. An ignore file and a careful staged-diff review help prevent the leak; neither can undo an exposure. :::

Walkthrough: save your local Python project​

Apply the same steps to the myproject folder and hello.py file you made in Run Python locally. Run python hello.py to check that it still works. This is a separate folder from the hello-git example above. Here's the workflow, with example output.

1. Move into the project and initialize Git:

cd myproject # from its parent folder; skip this if you're already inside
git init
Initialized empty Git repository in /Users/you/myproject/.git/

2. Add a .gitignore first to leave untracked venv and secret files out of normal staging. Create .gitignore with the Python contents shown above.

3. See what Git will track:

git status
On branch main

No commits yet

Untracked files:
(use "git add <file>..." to include in what will be committed)
.gitignore
hello.py

nothing added to commit but untracked files present

Notice .venv/ is not in that list — .gitignore is doing its job for that untracked folder. Check the list yourself; if you created other files, your output will differ.

4. Stage the intended files, review them, then commit:

git add .gitignore hello.py
git diff --staged

Read the diff: it should contain only the ignore rules and the hello.py code you intend to save. Press q if the diff fills the terminal and opens a scrollable view. Once you've checked it, commit:

git commit -m "Add hello script and ignore rules"
[main (root-commit) 7c3f1a9] Add hello script and ignore rules
2 files changed, 16 insertions(+)
create mode 100644 .gitignore
create mode 100644 hello.py

Naming the files makes your intent explicit. The shorthand git add . stages changes under the current folder, including changes to already tracked files; ignore rules only leave out matching untracked files. Review the staged diff whichever form you use.

5. Create an empty repo on GitHub named myproject (no auto README), copy its URL, then connect and push:

git remote add origin https://github.com/yourname/myproject.git
git branch -M main
git push -u origin main
...
To https://github.com/yourname/myproject.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.

Your small Python project now has a saved history and an online copy. 6. For each improvement, edit hello.py, run it, then stage the change, inspect git diff --staged, commit, and push when you want to update the online copy.

After you build the capstone, apply this workflow in its grade-report folder: add ignore rules, stage the intended grades.py and sample grades.json, review the diff, and commit. You can then connect that project to its own GitHub repository using the same remote-and-push steps.

Why it matters​

Version control is not optional in professional software — it's the floor. Every team uses Git; every job posting assumes it; every project in the guides above this one starts with a repo and ends with a push. Beyond jobs, it's the thing that lets you experiment fearlessly: try a wild change, and if it breaks, rewind to the last good commit. And GitHub turns your work into something you can show — the difference between "I learned to code" and "here are three working projects, look at the commit history." When the final lesson says put your projects on GitHub, this is the skill it means. You can do it now.

Common pitfalls​

Calmly, the mistakes nearly every beginner hits — and exactly what to do:

  • Committing a secret. You git add . with an API key in secrets.json and push it. Prevention: add ignore rules before staging, and inspect git diff --staged before committing. Ignore rules do not protect already tracked files. If a key is exposed, rotate it at the provider (see the warning above) — deleting the file isn't enough.
  • Forgetting to git add. You commit, but a changed file isn't in the snapshot because you never staged it. git commit only captures what's in the staging area. Habit: run git status right before committing and check the "Changes to be committed" list is what you expect.
  • "detached HEAD" panic. Sometimes git checkout <some-commit-id> (jumping to an old snapshot) drops you into a detached HEAD state, and Git prints a scary paragraph. Don't panic — it just means "you're looking at an old snapshot, not a branch." Nothing is broken and nothing is lost. To get back to normal, type git switch main (or git checkout main). That's the whole fix.
  • Merge conflicts. When two changes touch the same lines of a file (e.g. you edited a line on your laptop and the GitHub copy has a different edit on that line), a git pull can't decide which to keep, so it pauses with a merge conflict. Git marks the spot in the file like this:
    <<<<<<< HEAD
    your version of the line
    =======
    the other version of the line
    >>>>>>> origin/main
    This is not an error you broke — it's Git politely asking you to choose. Open the file, delete the <<<<<<<, =======, >>>>>>> marker lines, edit the text to the version you actually want, save, then git add the file and git commit. As a solo beginner you'll rarely hit these; when you do, they're a small editing task, not a catastrophe.
  • Forgetting to push. Commits live only on your machine until you push. If your "backup" matters, end your session with git push.

Checkpoint​

Required checkpoint

Version control with Git and GitHub

Pass to unlock the Next button below

Where this leads: with Git and GitHub in hand, the capstone you build next can be pushed online as a real portfolio piece — and every guide above this one starts from a repo. See Where this leads next, and revisit running Python locally for the venv-and-run loop this builds on.

Next: Capstone project →