PythonMastery
beginner 24 min read · lesson 11 of 14 in Python How-To

Git: Version Control for Your Python Projects

1 · The lesson

read

Git keeps every version of your project. Make a change, save a snapshot, and you can always get back to any earlier snapshot: the one from before the refactor, the one that still worked yesterday, the one you sent to a friend. It is also how almost every team shares code, and the history it keeps is the first thing many hiring managers open on your GitHub profile.

You don't need most of Git. About eight commands cover nearly everything a working developer types in a normal week, and this lesson is built around them, using one small project the whole way through: a tip calculator.

Git runs on your machine, not in the browser, so the blocks below are commands to type in a terminal (new to it? The Terminal for Python Developers takes twenty minutes). Every output shown is real, copied from running them. Your commit hashes (the 0ff4f3a style codes) will be different from these; everything else should match.


1. Set It Up Once

Check that Git is installed:

bash
git --version

No version number? Install it: on Windows from git-scm.com (it brings Git Bash with it), on macOS with xcode-select --install or brew install git, on Linux with your package manager (sudo apt install git).

Then tell Git who you are. This name and email go into every commit you make, so use the email you'll use on GitHub:

bash
git config --global user.name "Ada Lovelace"
git config --global user.email "ada@example.com"
git config --global init.defaultBranch main

The last line names your first branch main, which is what GitHub uses. You run these three once per computer, not once per project.


2. Your First Repository

A repository (repo) is a project folder that Git is watching. Make one:

bash
mkdir tip-calculator
cd tip-calculator
git init
text
Initialized empty Git repository in .../tip-calculator/.git/

Git created a hidden .git folder. That folder is the repository: the whole history lives in it. Delete it and the folder is just a folder again.

Now create tip.py:

python
def tip(bill, percent):
    return round(bill * percent / 100, 2)

print(tip(48.50, 15))

And ask Git what it sees. git status is the command you'll type most; when in doubt, type it.

bash
git status
text
On branch main

No commits yet

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

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

Untracked means Git can see the file but isn't saving its history yet. Notice that Git tells you the next command in brackets. It does that a lot, and it's usually right.


3. Save a Snapshot: add, Then commit

Saving takes two steps, and the reason is worth knowing.

WhereWhat it isCommand that moves things there
Working folderyour files as they are right nowyou, editing
Staging areathe changes you've picked for the next snapshotgit add
Historythe snapshots (commits) you've savedgit commit

The staging area lets you choose. Changed five files but only two belong to "fix the login bug"? Add those two, commit them, then deal with the rest separately.

bash
git add tip.py
git status
text
On branch main

No commits yet

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

Now commit, with a message that says what the snapshot does:

bash
git commit -m "Add tip calculator"
text
[main (root-commit) 0ff4f3a] Add tip calculator
 1 file changed, 4 insertions(+)
 create mode 100644 tip.py

0ff4f3a is the start of this commit's hash, its unique name. You'll use it to point at this exact snapshot later.

git add . stages everything that changed in the folder. It's handy, and it's also how passwords end up on GitHub. Section 5 fixes that before it happens.


4. See What Changed

Edit tip.py so a negative bill is refused:

python
def tip(bill, percent):
    if bill < 0:
        raise ValueError("bill can't be negative")
    return round(bill * percent / 100, 2)

print(tip(48.50, 15))

git diff shows exactly what you changed since the last commit. Lines starting with + were added, - removed:

bash
git diff
text
diff --git a/tip.py b/tip.py
index 3aec7e5..cf9869d 100644
--- a/tip.py
+++ b/tip.py
@@ -1,4 +1,6 @@
 def tip(bill, percent):
+    if bill < 0:
+        raise ValueError("bill can't be negative")
     return round(bill * percent / 100, 2)
 
 print(tip(48.50, 15))

Read your diff before every commit. It's the cheapest code review there is, and it's where you spot the print("HERE") you forgot to remove.

bash
git add tip.py
git commit -m "Reject negative bills"
git log --oneline
text
2170ffc Reject negative bills
0ff4f3a Add tip calculator

git log --oneline is the history, newest first. Plain git log shows the author and date too; press q to get out of it.


5. .gitignore: What Never Goes In

A Python project folder fills up with things that must not be committed: your virtual environment, Python's cache files, and the .env file holding your API keys. Without a .gitignore, Git offers them all to you:

bash
git status --short
text
?? .env
?? .venv/
?? __pycache__/

Create a file called .gitignore in the project folder:

gitignore
.venv/
__pycache__/
*.pyc
.env
bash
git status --short
text
?? .gitignore

They're gone from the list, and git add . will now leave them alone. Commit the .gitignore itself, so everyone who clones the project gets the same rules. Make it the first file in every new project, before git add . has a chance to pick up a secret. GitHub's Python .gitignore template covers far more cases if you want a complete one.


6. Branches: Try Something Without Breaking main

A branch is a separate line of commits. You make one when you start a feature, commit to it as much as you like, and main stays exactly as it was until you decide to bring the work in.

bash
git switch -c split-bill
text
Switched to a new branch 'split-bill'

-c creates the branch. Add a function to tip.py and commit it on this branch:

python
def split(bill, people, percent=15):
    return round((bill + tip(bill, percent)) / people, 2)
+ setup added so this can run · defines tip
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def tip(*_a, **_kw):
    print('-> tip() called')
    return _AutoMock('tip()')
bash
git add tip.py
git commit -m "Add split() for sharing a bill"

Switch back to main and the function is gone from the file, because main never had it. Switch to split-bill and it's back. When the feature works, merge it into main:

bash
git switch main
git merge split-bill
text
Updating 9aac7cf..f549246
Fast-forward
 tip.py | 4 ++++
 1 file changed, 4 insertions(+)

Fast-forward means main hadn't moved since you branched, so Git simply moved it forward to include your commits. The branch has done its job; delete it:

bash
git branch -d split-bill
text
Deleted branch split-bill (was f549246).

-d refuses to delete a branch whose work isn't merged yet, so it's safe to type.


7. Merge Conflicts (and Why They're Fine)

A conflict happens when two branches change the same line in different ways, and Git can't know which one you want. Here, main labelled the output and a branch called pounds added a currency, both on the last line:

bash
git merge pounds
text
Auto-merging tip.py
CONFLICT (content): Merge conflict in tip.py
Automatic merge failed; fix conflicts and then commit the result.

Nothing is broken and nothing is lost. Git has written both versions into the file and marked them:

text
<<<<<<< HEAD
print("Tip:", tip(48.50, 15))
=======
print(f"Tip: £{tip(48.50, 15)}")
>>>>>>> pounds

Above ======= is your current branch (HEAD); below it, the branch you're merging in. Edit the file into what it should be, keeping one, the other or a mix, and delete all three marker lines. Here the currency version wins, so the top of tip.py reads:

python
def tip(bill, percent):
    if bill < 0:
        raise ValueError("bill can't be negative")
    return round(bill * percent / 100, 2)

print(f"Tip: £{tip(48.50, 15)}")

Then stage and commit, and the merge is finished:

bash
git add tip.py
git commit -m "Merge pounds: keep the currency"

Changed your mind halfway? git merge --abort puts everything back to before you typed git merge. Editors such as VS Code highlight the two versions and give you "Accept Current" and "Accept Incoming" buttons, which is the same edit with fewer keystrokes.


8. Undo

Most Git fear is fear of undo. These four cover the situations you'll actually meet.

Throw away changes you haven't committed. You've been experimenting with tip.py and want the last committed version back:

bash
git restore tip.py

The file goes back to how it was at the last commit. This one really does throw your changes away: there is no copy of them anywhere, so be sure.

Take a file back out of the staging area. You ran git add on something you didn't mean to:

bash
git restore --staged notes.txt

The file stays exactly as it is; it just isn't part of the next commit any more.

Fix the last commit's message (or add a forgotten file to it), before you've pushed it:

bash
git commit --amend -m "Add TODO list"
text
[main c47f5b6] Add TODO list
 Date: Wed Oct 7 12:10:00 2026 +0530
 1 file changed, 1 insertion(+)
 create mode 100644 TODO.md

Amending replaces the commit with a new one; note the new hash. That's harmless on your machine and confusing for everyone else if they already have the old one, so only amend what you haven't pushed.

Undo a commit that's already shared. git revert doesn't delete anything. It makes a new commit that does the opposite of the old one, so history stays honest and nobody's copy breaks:

bash
git log --oneline -2
text
3e6f0f8 Add debug print
c57c35f Merge pounds: keep the currency
bash
git revert --no-edit HEAD
text
[main 8116b70] Revert "Add debug print"
 1 file changed, 1 deletion(-)

HEAD means "the commit you're on now". To revert an older one, use its hash: git revert 3e6f0f8.

You'll see git reset --hard recommended online for all of this. It works, and it also deletes uncommitted work without asking. Learn it later; the four above are enough for now.


9. GitHub: Your Project Online

Git is the tool; GitHub is a website that hosts Git repositories. It's where your code gets backed up, shared, and looked at by people deciding whether to hire you.

1. Create a free account at github.com.
2. Click New repository, name it tip-calculator, and leave every "Initialize this repository with…" box unticked (your project already has its files).
3. GitHub shows you the address of the new repo. Connect your project to it and upload:

bash
git remote add origin https://github.com/ada/tip-calculator.git
git push -u origin main

origin is the conventional nickname for "the copy on GitHub". -u remembers the pairing, so from now on git push on its own is enough.

Logging in. GitHub stopped accepting account passwords for git push in August 2021. On Windows, Git comes with Git Credential Manager, so your first git push simply opens a browser window to sign in. On macOS and Linux, the easiest route is GitHub's own command-line tool: install gh, run gh auth login once, and choose HTTPS. Both are what GitHub's documentation recommends; it also covers the alternatives, a personal access token or an SSH key.

The daily loop after that:

bash
git pull          # get commits from GitHub that you don't have yet
# ...edit, add, commit...
git push          # send your new commits up

And to get a copy of any repository, yours or anyone's:

bash
git clone https://github.com/ada/tip-calculator.git

10. A History Worth Reading

Your commit history is a record of how you work, and it's public. Two habits make it worth reading:

Small commits that each do one thing. "Reject negative bills" is a good commit. "Updates" containing a new feature, a bug fix and a renamed folder is three commits squashed into one you can't undo separately.

Messages that say what the commit does. Write them as an instruction, the way Git writes its own (Revert "Add debug print", Merge branch ...): "Add split() for sharing a bill", not "added stuff" or "fix". Keep the first line under about 50 characters. If a change needs explaining, leave out -m; Git opens an editor where you write the short line, a blank line, then as much explanation as you like.

A good test: could someone read only your git log --oneline and tell what the project does and how it grew? On a hiring manager's two-minute look at your GitHub, that's exactly what they're doing.


Common Mistakes

1. Committing a secret

An API key in config.py, a .env file, a password in a test. Once it's pushed, treat it as stolen, even if you delete it a minute later: the old commit still contains it, and bots scan GitHub for keys within minutes. Revoke the key first (generate a new one where it came from), then remove it from the code and add the file to .gitignore. The secrets lesson covers keeping them out properly.

2. Committing .venv/ or __pycache__/

Hundreds of megabytes that only work on your machine. If it already happened: add them to .gitignore, then git rm -r --cached .venv to stop tracking the folder without deleting it from your disk, and commit.

3. Working directly on main for everything

Fine for a solo project you're learning on. The moment there's something you don't want to break, make a branch. It costs one command.

4. Panic over a conflict

CONFLICT is not an error, it's a question: "these two changes touched the same line, which do you want?" Answer it in the file, git add, git commit. Or git merge --abort and come back later.

5. "Fix", "update", "asdf"

Messages like these make your history useless to the one person who reads it most: you, three months from now, trying to find when something broke.

6. Amending or force-pushing commits other people already have

Rewriting shared history breaks everyone else's copy. On anything you've pushed, use git revert. If a command tells you to add --force, stop and work out why first.


🎯 Your Turn — Put a Project on GitHub

Take any project from this site (the CLI calculator is a good one) and give it a proper home.

Requirements:

1. A repository with a .gitignore committed first, before any code.
2. At least four commits, each doing one thing, with messages that say what.
3. One feature built on a branch and merged into main.
4. A README.md that says what the project does, how to run it, and one decision you made and why.
5. Pushed to GitHub, so the link opens for someone else.

Hint 1 — The order of the first commands git init, then create .gitignore, then git add .gitignore and git commit. Only after that add your code. That way nothing secret or generated can sneak into the very first commit.
Hint 2 — What goes in the README Three short sections are enough: a sentence on what it does, the exact commands to run it (python calculator.py), and a "Why" paragraph. The why is what most people skip, and what makes the project read as yours rather than a copied tutorial.
Show full solution
bash
mkdir cli-calculator && cd cli-calculator
git init

# 1. ignore rules first
printf '.venv/\n__pycache__/\n*.pyc\n.env\n' > .gitignore
git add .gitignore
git commit -m "Add .gitignore for Python"

# 2. the code, in small steps
#    (write calculator.py with basic + - * /)
git add calculator.py
git commit -m "Add calculator with the four basic operators"

#    (add the history feature)
git add calculator.py
git commit -m "Keep a history of results"

# 3. a feature on a branch
git switch -c safe-eval
#    (replace eval() with ast-based parsing)
git add calculator.py
git commit -m "Parse expressions safely instead of eval()"
git switch main
git merge safe-eval
git branch -d safe-eval

# 4. the README
#    (write README.md: what, how to run, why safe parsing)
git add README.md
git commit -m "Add README with usage and design notes"

# 5. online (create the empty repo on github.com first)
git remote add origin https://github.com/<you>/cli-calculator.git
git push -u origin main

git log --oneline

Five commits, each one readable on its own, and a README that explains the one real decision in the project. That's a repository someone can judge you by, in a good way.


What You Learned

  • A repository is a folder Git watches; the history lives in its .git folder.
  • Saving is two steps: git add picks the changes, git commit -m "..." saves the snapshot.
  • git status, git diff and git log --oneline tell you where you are, what changed and what happened. Use them constantly.
  • .gitignore keeps .venv/, __pycache__/ and .env out. Commit it first, in every project.
  • Undo: git restore (discard edits), git restore --staged (unstage), git commit --amend (fix the last unpushed commit), git revert (undo a shared commit safely).
  • Branches (git switch -c, git merge, git branch -d) let you build features without risking main.
  • A conflict is a question, not an error: edit the marked lines, add, commit.
  • GitHub: git remote add origin ..., git push -u origin main, then pull and push daily. Log in through the browser (Windows) or gh auth login.
  • Small commits with clear messages are a record of how you work, and people do read it.

Going deeper: this lesson covers what you'll use every week. When you want the rest (rebasing, how Git stores data internally, hosting your own server), Pro Git is the free, official book; chapters 2 and 3 cover the same ground as this lesson in more depth, and chapter 7 is where the power tools live.

Found this useful? Share it with a friend who’s learning Python.