Ignoring files with .gitignore
Keep secrets, build artifacts, and node_modules out of your repository using .gitignore patterns and git rm --cached.
What you'll learn
- What .gitignore is and where it lives
- How glob patterns, negation, and anchoring work in .gitignore
- The crucial difference between untracked files and already-committed files
Before you start
Why a clean repository matters
When someone clones your repository they should get source code — not your Python bytecode cache, not a 200 MB node_modules folder, and definitely not your API keys. A repository bloated with generated or secret files is slower to clone, harder to review, and potentially dangerous to share.
Git’s answer is .gitignore: a plain-text file that tells Git which files and folders to pretend do not exist.
Where .gitignore lives
Place .gitignore in the root of your repository. Git reads it automatically — no configuration needed.
my-project/
├── .gitignore ← the main one
├── src/
│ └── app.py
└── data/
└── .gitignore ← optional: rules for this subdirectory only
You can also place a .gitignore inside any subdirectory. Rules in a nested .gitignore apply only within that subdirectory. For most projects a single root-level file is all you need.
Pattern syntax
Each line in .gitignore is a glob pattern — a wildcard expression similar to what your shell uses for filename matching.
| Pattern | What it ignores |
|---|---|
*.log | Any file ending in .log, anywhere in the tree |
build/ | The entire build directory (trailing / means directory) |
# comment | Nothing — lines starting with # are comments |
!keep.log | Re-includes keep.log even if a prior rule excluded it (negation) |
/secret.txt | Only secret.txt at the repo root (leading / anchors to root) |
**/logs | A directory named logs anywhere in the tree (**/ means recursive) |
doc/**/*.pdf | All .pdf files inside doc/ and any of its subdirectories |
Glob means the pattern language where * matches any sequence of characters (except /), ? matches one character, and [abc] matches a set. It is not regular-expression syntax.
A realistic .gitignore
Here is a file you could drop into a Python project that also has a JavaScript build step:
# === Python ===
__pycache__/
*.py[cod]
*.pyo
.venv/
venv/
dist/
*.egg-info/
# === Secrets ===
.env
.env.*
!.env.example
# === Node / frontend ===
node_modules/
dist/
.next/
.nuxt/
# === OS files ===
.DS_Store
Thumbs.db
# === Editors ===
.vscode/
.idea/
*.swp
A few notes on the patterns above:
*.py[cod]uses a character class to match.pyc,.pyo, and.pydin one rule..env.*covers.env.local,.env.production, etc. The!.env.examplenegation re-includes the example file so teammates can copy it as a starting point.dist/appears under both Python and Node because both ecosystems write output there. Duplicate rules are harmless.
The crucial gotcha: tracked vs. untracked
The practical order of operations is:
- Create
.gitignorewith your patterns before your firstgit add. - If you already committed a file by accident, run
git rm --cachedand commit the removal. - Rotate any leaked credentials regardless of step 2.
The filter in action
Ready-made templates
You rarely need to write a .gitignore from scratch. Two authoritative sources:
- github/gitignore — the official collection at
github.com/github/gitignore. Each file is named after a language or framework (Python.gitignore,Node.gitignore, etc.). - gitignore.io (also at
toptal.com/developers/gitignore) — generates a combined file for multiple environments at once. Search “Python macOS VSCode” and download the result.
Copy the relevant template into your repo root, trim anything that does not apply, and add your own project-specific entries at the bottom.