.gitignore for Python

__pycache__/ *.py[cod] build/ dist/ *.egg-info/ .env .venv/ .pytest_cache/

A Python .gitignore ignores bytecode (__pycache__/, *.py[cod]), packaging output (build/, dist/, *.egg-info/), virtual environments such as .venv/, the .env file, and tool caches like .pytest_cache/. Those are the key lines of the tool’s Python template, which has 49.

What the blocks ignore

  • Bytecode and extensions: __pycache__/, *.py[cod] and *.so, all rebuilt from source.
  • Packaging: build/, dist/, wheels/, *.egg-info/, *.egg, and /lib/, /lib64/ and /var/, anchored to the root, where a virtualenv or buildout made in the project leaves them. A src/lib/ of yours stays.
  • Environments: .venv/, venv/, env/ and ENV/, plus .env, the file python-dotenv reads secrets from.
  • Tools: .pytest_cache/, .mypy_cache/, .ruff_cache/, .tox/, .nox/, coverage’s .coverage and htmlcov/, Jupyter’s .ipynb_checkpoints/, and PyInstaller’s *.spec and *.manifest.

In a scratch repository with this file, git check-ignore -v -n --no-index names the line that matched each path, or prints :: when none did:

.gitignore:3:__pycache__/	app/__pycache__/main.cpython-313.pyc
.gitignore:34:.venv/	.venv/bin/python
.gitignore:30:.env	.env
::	.env.example
.gitignore:10:dist/	dist/app-1.0-py3-none-any.whl
.gitignore:22:*.egg-info/	src/app.egg-info/PKG-INFO
::	poetry.lock
::	uv.lock
::	requirements.txt
.gitignore:15:/lib/	lib/python3.13/site-packages/six.py
::	frontend/src/lib/api.ts
.gitignore:40:*.spec	app.spec

Commit the lockfile and pyproject.toml

uv.lock, poetry.lock, pdm.lock and a pinned requirements.txt are what make every machine install the same versions; uv’s documentation says its lockfile belongs in version control. Commit .env.example too, with the variable names and no values.

The *.spec trap

PyInstaller writes app.spec on its first run, which is why the template ignores *.spec. Many projects then edit that file by hand, and with the line in place those edits never reach the repository. Re-include yours with a line after the template; it works because the file’s folder isn’t excluded:

.gitignore:63:!myapp.spec	myapp.spec
.gitignore:40:*.spec	other.spec

A ! line can’t reach inside an excluded directory: after .venv/, Git never looks in the folder, so no file in it can come back.

Bytecode was committed before the file?

.gitignore only affects untracked files, and __pycache__/ folders are scattered through the tree. Untrack everything, add back what the file allows, and commit; the working tree isn’t touched:

git rm -r --cached .
git add .
git commit -m "Apply .gitignore"

Editor and OS files

PyCharm’s .idea/, .vscode/ and .DS_Store come from your machine. Pick the JetBrains or VS Code template, or keep them in a global ignore file:

git config --global core.excludesFile ~/.gitignore_global

Try it in the Gitignore Generator

In the file: Python

.gitignore

1 template · 49 pattern lines
# Python
# Bytecode cache written next to the source
__pycache__/
*.py[cod]
*$py.class
# Compiled extension modules built in place
*.so
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
# Left at the root by a virtualenv or buildout made there; anchored, so a lib/ further down, such as src/lib/, stays
/lib/
/lib64/
parts/
sdist/
/var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
pip-log.txt
pip-delete-this-directory.txt
.Python
# Local environment variables, read by python-dotenv and often holding secrets
.env
# Virtual environments: env, venv, .venv, ENV and the .bak variants
env/
venv/
.venv/
ENV/
env.bak/
venv.bak/
# Files PyInstaller writes before it builds an executable
*.manifest
*.spec
.pytest_cache/
.mypy_cache/
.ruff_cache/
.tox/
.nox/
.dmypy.json
dmypy.json
# Output of the Pyre and Pytype type checkers
.pyre/
.pytype/
cython_debug/
# IPython profile and history, when it writes into the project
profile_default/
ipython_config.py
# Data files from coverage.py, pytest-cov and friends
.coverage
coverage.xml
htmlcov/
# Checkpoints Jupyter writes next to every notebook
.ipynb_checkpoints/
  • ✓Not ignored: no line in this file matches "/uv.lock".

Lines are applied top to bottom, and the last one that matches wins — which is why a line of your own can overrule a template.

Runs in your browser. Nothing you enter is sent anywhere.