Python Git and Python Projects

Set up a Python project in Git with the right ignore rules, a clear folder layout, focused commits and a tested branch workflow.

Lesson 65 of 107Advanced12 min read

Git tracks the history of a project's source files, so you can see what changed, undo mistakes and work with others. A Python project in Git needs three things to be pleasant to work with: ignore rules that keep generated files out, a layout that separates code, tests and documentation, and a habit of focused commits checked by tests.

Text
nature-log/
    .gitignore
    pyproject.toml
    README.md
    src/nature_log/__init__.py
    src/nature_log/observations.py
    tests/test_observations.py

This lesson covers each part for a project that records wildlife observations. It assumes Git is installed and you know git add, git commit and git status; the Git course covers those in detail.

Repository boundaries

Commit source: code, tests, configuration (pyproject.toml, lock files), documentation and small sample data. Don't commit anything that can be regenerated or that is specific to one machine or person. A typical Python .gitignore:

Text
# virtual environments
.venv/
# bytecode and caches
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
.ruff_cache/
# build output
dist/
build/
*.egg-info/
# local settings and secrets
.env
.vscode/
.idea/

GitHub's github/gitignore repository has a maintained Python.gitignore you can start from.

An ignore rule only affects untracked files. If .env or __pycache__ was committed before you added the rule, Git keeps tracking it. Remove it from the index while keeping the local file with git rm --cached .env, then commit. If a secret was committed, also rotate it, because it remains in the history.

Project layout

Separate the parts of the project so each has an obvious home:

  • src/nature_log/: the importable package. The src layout prevents tests from accidentally importing the local folder instead of the installed package, which catches packaging mistakes early.
  • tests/: test files named test_*.py, mirroring the package modules.
  • pyproject.toml: metadata, dependencies and tool settings.
  • README.md: what the project does and how to set it up and run it.

With the src layout, install the project in your virtual environment in editable mode, python -m pip install -e ., so import nature_log works in tests and scripts. A simpler "flat" layout, with the package folder at the root, is also common for small projects and scripts.

Import behaviour depends on the layout and on how you start Python: python src/nature_log/cli.py and python -m nature_log.cli set up sys.path differently. Running modules with -m from an installed package avoids most ModuleNotFoundError surprises.

Commits

A good commit contains one coherent change and explains why it was made:

Terminal
git add src/nature_log/observations.py tests/test_observations.py
git commit -m "Reject observations with negative bird counts

Counts below zero came from a spreadsheet import bug and broke the
weekly totals. Validation now raises ValueError with the record id."

Commit the fix together with the test that proves it, so they can't drift apart. Keep unrelated changes, such as a reformat or a dependency update, in their own commits; a reviewer can then read each one in isolation, and a problem can be reverted without losing other work.

Use git add -p to stage parts of a file when you've made several changes at once, and git diff --staged to check exactly what you're about to commit. The first line of the message should be a short summary in the imperative ("Reject...", "Add..."), around 50 characters.

Collaboration

Teams usually work on branches: create one for a change, commit there, push it, and open a pull request for review.

Terminal
git switch -c fix/negative-counts
# edit, test, commit
python -m pytest
ruff check .
git push -u origin fix/negative-counts

Run the project's checks before pushing: tests, linter, formatter and type checker. Continuous integration runs them again on the pull request, across Python versions if configured, so reviewers know the change works before reading it.

A merge without conflicts only means Git could combine the text; it doesn't prove the combined program works. Two branches can each pass their tests and still break when merged, which is why CI runs on the merged result and why tests matter more than a clean merge. Tools such as pre-commit run formatting and lint checks automatically on every commit, catching problems before they reach the shared repository.

Independent practice and a worked check

Task: create a documented Python project layout with ignore rules, a focused commit and a tested feature branch.

Terminal
mkdir -p nature-log/src/nature_log nature-log/tests && cd nature-log
git init
printf ".venv/\n__pycache__/\n*.pyc\n.env\n" > .gitignore
printf 'def total(counts):\n    return sum(counts)\n' > src/nature_log/observations.py
touch src/nature_log/__init__.py
printf 'from nature_log.observations import total\n\ndef test_total():\n    assert total([2, 3]) == 5\n' > tests/test_observations.py
git add . && git commit -m "Create project with observation totals and a test"
git switch -c feature/validation

With a minimal pyproject.toml and python -m pip install -e . in a virtual environment, python -m pytest runs the test before you push the branch. For the worked check: should a project normally commit its .venv directory? No. Commit the dependency files and recreate the environment instead.

Reference and next practice

The Git documentation, GitHub's Python .gitignore template and the packaging guide's src layout discussion are useful references. Continue with interview questions and the Git course. Practise with the Python exercises and Python quiz.

Git and Python Projects Summary

  • Repository boundaries: Git records selected source files rather than every generated environment artifact.
  • Project layout: Separate application modules, tests and supporting documentation.
  • Commits: Focused commits describe one coherent behavior change with a useful message.
  • Collaboration: Branches and review help integrate work, while tests check the resulting combined behavior.

Git and Python Projects Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Git and Python Projects in a small program

A small script that inspects the project's tracked files.

Python
from pathlib import PurePath
tracked = ["src/observations.py", "tests/test_observations.py", "README.md"]
for file_name in tracked:
    print(PurePath(file_name).suffix)
Output
.py
.py
.md
Line-by-line explanation
  1. Line 1PurePath handles path text without touching the disk.
  2. Line 2The tracked files of a minimal project: a module, its tests and a README.
  3. Line 4.suffix gives each file's extension.
Example 2

Checking files against ignore patterns

  • Run git check-ignore -v <path> in a real repository to see which rule ignores a file.
Python
from fnmatch import fnmatch
from pathlib import PurePath

ignore = [".venv/*", "*/__pycache__/*", "*.pyc", ".env", "dist/*"]
candidates = [
    "src/nature_log/observations.py",
    "src/nature_log/__pycache__/observations.cpython-313.pyc",
    ".venv/bin/python",
    ".env",
    "tests/test_observations.py",
    "pyproject.toml",
]

for path in candidates:
    ignored = any(fnmatch(path, pattern) for pattern in ignore)
    print(f"{'ignore' if ignored else 'commit':<7} {PurePath(path).name}")
Output
commit  observations.py
ignore  observations.cpython-313.pyc
ignore  python
ignore  .env
commit  test_observations.py
commit  pyproject.toml
Line-by-line explanation
  1. Line 4Simplified patterns similar to .gitignore rules; Git's real matching has more rules, such as directory-only patterns ending in /.
  2. Line 15any() stops at the first matching pattern.

Common Git and Python Projects Mistakes

Adding an ignore rule for a file that's already tracked

Don't
echo ".env" >> .gitignore
git commit -am "Ignore .env"     # .env is still tracked and still committed
Do
echo ".env" >> .gitignore
git rm --cached .env
git commit -m "Stop tracking .env"

One commit for many unrelated changes

Don't
git add -A
git commit -m "updates"
Do
git add src/nature_log/observations.py tests/test_observations.py
git commit -m "Reject negative bird counts"
git add pyproject.toml requirements.lock
git commit -m "Update requests to 2.32"

Git and Python Projects in Real Projects

Git workflows for Python teams

Most Python teams use a pull-request workflow on GitHub, GitLab or Bitbucket: short-lived branches, automated checks with GitHub Actions or similar, at least one review, then a merge to the main branch. A .pre-commit-config.yaml file runs Ruff, Black and other hooks on every commit, and a CI workflow runs pytest and mypy on several Python versions.

Releases are tagged (git tag v1.2.0), and changelogs are often generated from commit messages, another reason to write clear ones. Open-source projects add CONTRIBUTING.md with exactly these conventions so new contributors can follow them.

Git and Python Projects FAQ

What should be in a Python .gitignore?

At minimum: virtual environment folders (.venv/), bytecode (__pycache__/, *.pyc), tool caches (.pytest_cache/, .mypy_cache/), build output (dist/, build/, *.egg-info/) and local secrets such as .env. GitHub's Python template is a good starting point.

Should I commit __pycache__ folders?

No. They contain compiled bytecode that Python regenerates automatically, differs between Python versions and only adds noise to diffs. Ignore them.

What is the src layout in Python?

A project structure where the importable package lives in src/package_name/ instead of at the root. It forces tests to import the installed package, which catches packaging mistakes, and avoids accidentally importing the working directory.

How do I remove a file from Git but keep it locally?

Run git rm --cached path, add the path to .gitignore, and commit. The file stays on your disk but is no longer tracked. Remember it's still in earlier commits, so rotate any secret it contained.

Should I commit requirements.txt or a lock file?

Yes. Commit pyproject.toml and your lock file or pinned requirements so everyone, including CI and production, installs the same versions. Don't commit the virtual environment itself.

Git and Python Projects Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Should a project normally commit its .venv directory?

Multiple choicemedium

Which review concern is valid for git and python projects?

Git and Python Projects Quiz

2 questions to check what you learned.

  1. Should a project normally commit its .venv directory?

    • Yes
    • No
    • Python ignores the distinction
  2. Which statement correctly describes commits?

    • Focused commits describe one coherent behavior change with a useful message.
    • Only the example output matters, not the rule
    • Every operation has the same behavior