Python Write JSON to File With Pretty Print (Tested)
Save a Python dict as a readable JSON file with json.dump(indent=4), keep £ and é as they are, handle datetimes, overwrite safely and append the right way.
To remove a file from a Git repository but keep it on your computer, run git rm --cached <file>, add the file to .gitignore, and commit. The --cached flag tells Git to remove the file from the index (what Git tracks) but leave your working copy alone, so the file stays on disk while Git stops tracking it. For a folder such as node_modules, add -r: git rm -r --cached node_modules. Below I run every command on a real throwaway repo and show the exact output, including the two surprises: what happens to your teammates' copies, and why a committed password isn't safe even after this.
The first time I needed this was the classic beginner mistake. I'd committed a .env file with an API key in it, along with the whole node_modules folder. I wanted both out of the repo, but I still needed them locally to run the project. All the commands below were run with Git 2.47 in a script, and the output is pasted straight from it.
Here's the setup: a repo whose first commit accidentally included .env and node_modules. Then git rm --cached removes .env from Git only:
$ git ls-files
.env
app.js
node_modules/left-pad/index.js
# Remove one file from the repo, keep it locally
$ git rm --cached .env
rm '.env'
$ git status --short
D .env
?? .env
$ ls -a
.
..
.env
.git
app.js
node_modules
# Stop it coming back
$ echo ".env" >> .gitignore
$ git add .gitignore
$ git commit -q -m "Stop tracking .env"
$ git status --short
Read the git status --short lines carefully, because they explain exactly what --cached does:
D .env (in the first column) means the deletion of .env is staged. The next commit will remove it from the repository.?? .env means the same file is still on disk, and Git now sees it as untracked.ls -a confirms the file is still there. After adding .env to .gitignore and committing, git status is clean: Git no longer tracks the file and has been told to ignore it from now on.
Without --cached, git rm .env would delete the file from your disk as well. That's the one-word difference between "stop tracking" and "delete", so double-check it before pressing Enter.
This is the part that confuses most people, including me the first time. .gitignore only affects files Git isn't tracking yet. If a file was already committed, adding it to .gitignore does nothing: Git keeps tracking it and keeps showing your changes.
You can see that in this second test repo. debug.log, dist/ and .DS_Store were committed first, then added to .gitignore. git status shows only the new .gitignore, and Git carries on tracking the three files:
# .gitignore is ignored for files Git already tracks
$ printf "*.log\ndist/\n.DS_Store\n" > .gitignore
$ git status --short
?? .gitignore
# Preview with -n (dry run)
$ git ls-files -ci --exclude-standard
.DS_Store
debug.log
dist/app.js
$ git rm -r --cached -n .
rm '.DS_Store'
rm 'debug.log'
rm 'dist/app.js'
rm 'index.php'
That's why you always need both steps: git rm --cached to stop tracking the file now, and .gitignore to stop it being added again by the next git add ..
For a folder, add -r (recursive). Here node_modules comes out of the repo but stays on disk:
# A whole folder
$ git rm -r --cached --quiet node_modules
$ echo "node_modules/" >> .gitignore
$ git add .gitignore
$ git commit -q -m "Stop tracking node_modules"
$ git ls-files
.gitignore
app.js
$ ls node_modules/left-pad
index.js
--quiet just stops Git listing every file it untracks, which for a real node_modules can be thousands of lines. The trailing slash in node_modules/ in .gitignore means "only match a folder with this name".
When you add a proper .gitignore to an old project, you often want to untrack every file that it covers in one go. A popular answer online is git rm -r --cached . followed by git add .. Look at the dry run (-n) in the output above, though: it would also untrack index.php, a file you want to keep tracking. The git add . puts it back, but that's a lot of churn if something goes wrong in between.
A more precise approach is to ask Git which tracked files are ignored, and remove only those:
# list tracked files that .gitignore now matches
git ls-files -ci --exclude-standard
# untrack exactly those files (they stay on disk)
git ls-files -ci --exclude-standard -z | xargs -0 git rm --cached
Here's the rest of the test run, using that command, followed by an accidental git rm --cached that I undo:
# Untrack only the ignored files
$ git ls-files -ci --exclude-standard -z | xargs -0 git rm --cached
rm '.DS_Store'
rm 'debug.log'
rm 'dist/app.js'
$ git add .gitignore
$ git status --short
D .DS_Store
A .gitignore
D debug.log
D dist/app.js
# Unstage a mistaken git rm --cached
$ git rm --cached keep.txt
rm 'keep.txt'
$ git restore --staged keep.txt
$ git status --short
The list was .DS_Store, debug.log and dist/app.js, and after removing them git status shows exactly those three deletions plus the new .gitignore, ready to commit. index.php is untouched. The -z and -0 pair handles filenames with spaces. On Windows, run it in Git Bash.
If you haven't committed yet, unstage the removal with git restore --staged <file>. That puts the file back into the index as if nothing happened (the end of the output above shows a clean status after it). If you've already committed, use git revert on that commit, or check the file in again with git add -f <file> if it's in .gitignore. My post on how to undo the last Git commit but keep your changes covers the commit side of this in detail.
git rm --cached keeps the file on your disk. Everyone else who pulls your commit gets a deletion, and Git deletes the file from their working copy. I tested this with a second clone acting as a teammate:
# in the teammate's clone
$ ls
app.js
config.env
$ git pull -q
$ ls
app.js
config.env simply disappears from the teammate's folder after git pull. For a generated folder like node_modules, that's harmless; they run npm install again. For a local config file, it can break their setup. Warn people before you push, and ask them to back the file up, or recover it afterwards with git show HEAD~1:config.env > config.env, which writes the old copy back without staging it (the new .gitignore then keeps it untracked).
The last line of the first test is the important one:
$ git show HEAD~2:.env
API_KEY=secret123
git rm --cached removes the file from future commits, not from old ones. Anyone with a copy of the repo can still read the key from history, and if the repo was ever public on GitHub, assume bots have already scanned it. So the order is:
git rm --cached and .gitignore.git filter-repo, then force-push and ask everyone to re-clone. It rewrites every commit hash, so treat it as a last resort.The best fix is not committing secrets at all. My post on adding a Gemini API key to Next.js and Vercel shows how I keep keys in environment variables, and how I kept my AI API key off the browser covers keeping them away from the front end.
You'll see git update-index --assume-unchanged and --skip-worktree suggested for "keep a file locally". They're different: the file stays in the repo, and Git just stops noticing your local edits. That's useful for a shared config file you tweak locally but never want to commit. It isn't a way to remove a file from the repository, and the flags are easy to forget about, so I only use git rm --cached for files that shouldn't be in Git at all.
Run git rm --cached path/to/file, add the path to .gitignore, then commit. The file stays on your disk and Git stops tracking it.
git rm removes the file from the repository and deletes it from your disk. git rm --cached only removes it from the repository (the index) and leaves your local file untouched.
Because .gitignore only applies to untracked files. Run git rm --cached on the file once, commit, and from then on .gitignore keeps it out.
Yes. When teammates pull the commit, Git deletes the file from their working copies. Tell them first, and they can restore it from the previous commit if they need it.
No. Old commits still contain it. If it held a password or API key, rotate the key, and use git filter-repo only if the history itself must be cleaned.
The official git rm documentation explains every flag.
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
Save a Python dict as a readable JSON file with json.dump(indent=4), keep £ and é as they are, handle datetimes, overwrite safely and append the right way.
Hash passwords in PHP with password_hash(), check them with password_verify(), and build a PDO register and login that upgrades old hashes. Real output.
Two UK postcode regex patterns for JavaScript tested on 14 inputs, a function that tidies 'sw1a1aa' into 'SW1A 1AA', and a form that uses it.