惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
Arctic Wolf
有赞技术团队
有赞技术团队
H
Help Net Security
N
Netflix TechBlog - Medium
G
Google Developers Blog
GbyAI
GbyAI
Jina AI
Jina AI
D
DataBreaches.Net
博客园 - Franky
Recent Announcements
Recent Announcements
博客园 - 叶小钗
大猫的无限游戏
大猫的无限游戏
N
News | PayPal Newsroom
S
SegmentFault 最新的问题
B
Blog RSS Feed
Google DeepMind News
Google DeepMind News
S
Schneier on Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
AWS News Blog
AWS News Blog
V
V2EX
月光博客
月光博客
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 【当耐特】
P
Privacy International News Feed
T
The Exploit Database - CXSecurity.com
云风的 BLOG
云风的 BLOG
F
Full Disclosure
Microsoft Security Blog
Microsoft Security Blog
MongoDB | Blog
MongoDB | Blog
V
Vulnerabilities – Threatpost
C
CERT Recently Published Vulnerability Notes
人人都是产品经理
人人都是产品经理
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
爱范儿
爱范儿
C
Cyber Attacks, Cyber Crime and Cyber Security
P
Privacy & Cybersecurity Law Blog
G
GRAHAM CLULEY
Spread Privacy
Spread Privacy
罗磊的独立博客
P
Proofpoint News Feed
The Last Watchdog
The Last Watchdog
S
Secure Thoughts
O
OpenAI News
P
Palo Alto Networks Blog
The Cloudflare Blog
Microsoft Azure Blog
Microsoft Azure Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tenable Blog
雷峰网
雷峰网
C
Cisco Blogs

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
How to safely remove a Django model field: finding every real reference before you delete
Kazu · 2026-06-02 · via DEV Community

Every Django project has at least one of these. A model with an old field that's probably not used anymore. "Probably" is the scary part. If something in production is still referencing it, deleting the column breaks the app. AttributeError, in production.

So you don't delete it. You want to, but you can't figure out how to check safely, so it just sits there. The column takes up space in every query. The field clutters the model definition. And it keeps sitting there, month after month, because the cost of confirming it's unused feels higher than the cost of leaving it.

Think about what happens when AttributeError: 'Article' object has no attribute 'summary' hits production. Users get 500 errors every time they open the page. Logs flood. Slack lights up. "Was it that deploy we just pushed?" Already considering a rollback. And the cause was deleting a field you thought was unused.

That's why nobody deletes anything. You can't be sure, so you don't. That's the right call. The problem is there was no way to get sure.

I tried the obvious thing: searching for the field name in VS Code. Hundreds of hits. I started opening them one by one and immediately noticed most aren't real references. File paths, comments, unrelated variable names. I searched for the .html field in one Django project and got 1,202 results. The actual code accessing that field: 10 results.

1,192 were noise. But you couldn't know that upfront.

Why search returns 1,202 hits

When you give up because there are too many results, that's not a failure on your part. VS Code search and grep just weren't built for this.

These tools answer "does this string appear anywhere in this file?" That's useful for a lot of things. But when you want to know "is this field actually referenced in code?", text search picks up way too much. The field name in a string literal, in a comment, as part of a filename: it all counts as a hit. Sorting through them is manual work.

Here's what those 1,202 results for .html broke down to:

Type Count
File extensions (layout.html, etc.) 1,087
Unrelated strings 27
Comments 21
Other noise 57
Actual field accesses 10

You can't check 1,200 results. Giving up was the right call. The issue wasn't how you used the tool. You were using the wrong tool.

And .html isn't a special case. Say you want to delete a title field on an Article model. "title" shows up everywhere in a codebase: variable names, dictionary keys, comments, strings. Hundreds of results. Same problem.

Common field names are the worst. status, name, type, created_at — these appear in dozens of unrelated contexts throughout a typical Django project. The search that was supposed to answer a simple question becomes 40 minutes of opening files, closing files, and losing track of what you've already checked.

"I searched, couldn't check everything, left it alone." Most Django developers have been here. You want to delete it but can't. The check isn't impossible, it's just too expensive. So the field stays. They accumulate.

This compounds over time. A project that's two years old might have thirty fields that nobody's confident about. The developers who added them have moved on. The tickets that motivated them are closed. The tests, if they exist, pass regardless of whether the field is used. There's no mechanism forcing a cleanup, just the gradual intuition that the schema is getting harder to understand.

If you know regex, you might try narrowing it: grep -rn "\bhtml\b" --include="*.py" to limit to Python files. Still the same problem. "html" as a dictionary key, in a comment — it all still hits. "Python files only" and "actual field access" are completely different things.

The more you refine the regex, the more you start wondering whether the regex itself is missing something. You end up needing to verify the verification. The tool that was supposed to save you time has become another source of doubt.

There's also the psychological cost. You open VS Code, run the search, see 847 results for status, and your shoulders drop. You close the tab. You tell yourself you'll check it later. "Later" never comes. Nobody should have to hand-verify 847 results to answer a yes/no question.

When undeletable fields pile up, here's what actually happens.

The schema bloats. Ten, twenty unused columns accumulate. A new developer opens the model, scans the fields. "What's this one for?" They try to find out, can't, and decide not to touch it. Reasonable. But when that pattern repeats, you end up with an unspoken rule: don't touch this model.

Every migration feels slightly more risky. The unease builds until nobody touches it at all. Six months later, another developer thinks the same thing. The cycle repeats.

This is technical debt in the same way missing tests are. An unresolvable cost that accumulates. Development slows. Onboarding takes longer. Nobody intended this, but the project gets heavier over time.

This isn't a skills problem. The tool didn't exist yet.

Here's the situation I kept ending up in: I'd look at a field, feel like it was probably unused, open VS Code to check, get overwhelmed by results, close it, and go do something else. The field would still be there six months later. The next developer would go through the same loop. The field would still be there a year later.

At some point the schema becomes archaeology. Fields with names like legacy_content, old_slug, deprecated_flag: nobody knows what they do, nobody wants to touch them, and the project carries them forever. Every SELECT * is slightly slower. Every new developer's mental model of the data is slightly more confused.

The real cost is cognitive load. Every unused field is a small tax on everyone who reads the model. Multiply that by thirty fields and two years of new developers and you start to see why "we never clean up old fields" becomes an invisible drag on velocity.

How colref reads code structure instead of text

What you actually wanted to know was: where is obj.html referenced in code? colref returns exactly that. File paths and string contents ignored.

How does it tell the difference? Instead of treating code as a sequence of characters, it reads the code structure.

embed.html in Python means "read the html attribute of the embed object": a specific structure. "pages/publish.html" is string data, not an attribute access. Reading code structure makes that difference detectable. Only places written as object.field_name get picked up. The .html that appears inside a string is ignored.

If text search is like pressing Ctrl+F on a page, reading code structure is closer to a human reading through every line. Except it handles thousands of lines in an instant.

It scans Python code and returns only .field_name accesses, with file and line number.

Method Hits (for .html) What it sees
VS Code full-text search 3,534 All string matches
grep \.html\b 1,202 Word-boundary matches
colref 10 Actual field accesses only

3,534 or 1,202 becomes 10. Whether you can act on the results depends entirely on how many there are.

When you get 10 results: open each one. views.py:42 means go to that line and check whether obj.html is actually being accessed. Real reference — can't delete. Not a real reference — skip. Ten results takes 10–15 minutes.

A few common things you'll see when reviewing results: the field name appearing in a migration file (colref skips migrations, but if it didn't, this would be a false positive; the migration is just recording the history of the field's existence, not actively using it). You might also see test factories or fixtures that set the field value. Worth noting: if you delete the field and forget to update the factory, your test suite will break. That's not a reason not to delete, it's just something to clean up as part of the deletion.

When you get zero: you have a fact. "No references found in Python code." That's different from "I think it's probably unused." Move to the next step: checking getattr, templates, Admin, Forms, and Serializers. Zero from colref is the starting point, not the finish line.

The shift is from "check 1,200 things" to "check 10 things, then a handful of specific files." That's the difference between a task you'll defer indefinitely and one you'll do today.

For the technical details of how code structure is read, see ARCHITECTURE.md.

Installation

Install via pipx:

pipx install colref

Enter fullscreen mode Exit fullscreen mode

Or with pip:

pip install colref

Enter fullscreen mode Exit fullscreen mode

Specify the model name, field name, and your project directory:

colref check --orm django --model Embed --field html ./

Enter fullscreen mode Exit fullscreen mode

Results come back as filename:line_number. Each one is something you can open directly. Ten results takes maybe ten minutes to verify. Nothing compared to scrolling through 1,202 results, losing your place, and giving up halfway through.

A note on model names: use the class name exactly as it appears in your models file, including capitalization. Embed, not embed or EMBED. Field names are case-sensitive too: html, not HTML. If you get zero results for a field you know is used, double-check the casing first.

The ./ at the end is the path to scan. You can point it at a specific app directory if you want to narrow it down, but pointing at the project root works fine and makes sure nothing gets missed.

What zero results doesn't cover

Zero results doesn't mean "safe to delete." It means "not found in Python code."

Dynamic access: getattr(obj, field_name) with the field name in a variable won't be detected. Check separately:

grep -rn "getattr" --include="*.py" ./ | grep your_field

Enter fullscreen mode Exit fullscreen mode

Django templates: {{ page.html }} lives in .html files. colref only scans .py. Check templates separately:

grep -rn "your_field" --include="*.html" ./

Enter fullscreen mode Exit fullscreen mode

Django Admin, Forms, and DRF Serializers: This is the easiest one to miss. None of these are detected:

# Django Admin
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title']
    list_filter = ['title']

# Django Forms
class ArticleForm(forms.ModelForm):
    class Meta:
        fields = ['title']

# DRF Serializer
class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        fields = ['title']

Enter fullscreen mode Exit fullscreen mode

Determining which model the string 'title' in a list refers to requires tracing class inheritance, which colref doesn't handle yet. Check these separately:

grep -rn "your_field" --include="*.py" ./

Enter fullscreen mode Exit fullscreen mode

This grep has the same noise problem as full-text search. Opening the Admin, Forms, and Serializer files directly is more reliable. In most projects there aren't many of them.

These are exactly the places a Django beginner might not think to check. You verify the views, the serializers feel obvious after you remember them, but Django Admin is easy to forget, especially if the admin configuration lives in a file you rarely open. I've seen list_display hold a reference to a field that had been "confirmed deleted" twice already. The admin file just wasn't in anyone's mental checklist.

Once you've checked all of the above and colref returns zero, that's a grounded deletion: confirmed in Python code, checked getattr, templates, Admin/Forms/Serializers. Not "I think it's probably fine."

Checking Admin/Forms/Serializers by eye sounds tedious, but in practice it takes a few minutes. These files tend to be organized by model. Open admin.py, search for the model name, check list_display and related attributes. Open serializers.py, find the relevant serializer, check fields. Open forms.py if you have one. It's not a grep problem, it's an "open three files and look" problem. That's manageable even without a tool.

colref (Python attribute accesses) + grep (dynamic patterns and templates) + manual check (Admin/Forms/Serializers) covers the vast majority of real-world Django codebases. There are edge cases colref doesn't handle yet; the Detection Patterns docs list them. For most projects, this three-part check is enough to move from "I think it's probably unused" to "I have confirmed it's unused."

The five-step procedure

# 1. Check for field accesses in Python code
colref check --orm django --model YourModel --field your_field ./

# 2. Check for dynamic access
grep -rn "getattr" --include="*.py" ./ | grep your_field

# 3. Check templates
grep -rn "your_field" --include="*.html" ./

# 4. Delete the field and generate the migration
python manage.py makemigrations --name remove_your_field

# 5. Apply to the schema
python manage.py migrate

Enter fullscreen mode Exit fullscreen mode

Steps 2 and 3 are still grep — colref doesn't solve everything. But step 1 cuts 1,202 results down to 10. The "too many results to check, left it alone" situation: this is the one place that changes.

The difference between "probably unused, I think" and "zero results in Python code, no getattr, nothing in templates" is real. If something breaks in production, knowing what you checked tells you exactly where to look. You know the cause came from outside your checked scope: a dynamic reference, a template, a pattern colref doesn't handle yet. The cause is narrowed. Grounded deletion makes debugging faster when things go wrong.

One more thing about step 4 and 5: don't skip makemigrations --name. Giving the migration a descriptive name like remove_summary_field makes the history readable. Six months from now, someone scanning migration filenames can see what changed and when without opening every file.

Also: run the migration locally and make sure your test suite passes before deploying. When you're confident about a deletion it's tempting to skip the verification. Don't. If a test factory is still setting the deleted field, the tests will catch it before production does.

The whole process — run colref, check the checklist, generate the migration, run tests locally, deploy — takes maybe 30 minutes for a field that's actually unused. Compare that to leaving the field there indefinitely because you couldn't confirm it was safe to remove.

First run: try a field you know is used

If you don't have a candidate field in mind, try a field you know is used, something like title on Article:

colref check --orm django --model Article --field title ./

Enter fullscreen mode Exit fullscreen mode

If title is in use, you'll get multiple results with file and line number:

app/views.py:42
app/serializers.py:18
app/templates/article_detail.py:11

Enter fullscreen mode Exit fullscreen mode

Seeing what a real result looks like makes it easier to judge zero results later. Then try a field you've been wondering about. Close to zero? Move to steps 2 and 3.

From installation to first run: under five minutes. No need to read the README first. Running it is faster than reading about it.

What do you do when you get 3 results? Open all three. For each one: is this code still running in production? If a reference is inside a function that's clearly dead code, something wrapped in if False or commented out, it doesn't count. If it's live code, the field is still in use. But 3 results is a manageable number. You can make that judgment call.

What if you get 0 results? Don't stop there. Run steps 2 and 3. Zero from colref, zero from getattr grep, zero from template grep: that's three independent checks. At that point, also check your Admin, Forms, and Serializer files by eye. If all of that is clear, you have something solid to stand on.

Even without a deletion candidate right now, colref is useful for routine schema review. Scan migration history, spot something that looks unused, run colref. Zero results? It goes on the deletion candidate list. "Probably unused" becomes "not referenced in Python code" in 30 seconds.

I do this periodically on projects I maintain. Every few months I scan the migration history for fields I don't recognize, run colref on them, and build a short list. It takes maybe 20 minutes and usually turns up one or two candidates worth investigating further. Some end up staying because they're used in ways colref doesn't detect yet. But a few always turn out to be genuinely gone: references removed over time, nobody noticed, nobody cleaned it up. Those get deleted.

colref is still in development. If something doesn't work or you get unexpected results, open an issue at github.com/shinagawa-web/colref. Real usage feedback is what shapes the priorities. A bug report with a concrete example is more useful than ten feature requests.

colref currently supports Django and Rails. For the roadmap, see issue #74.

If you find a field that colref misses, something it should have flagged but didn't, that's especially useful to report. The detection gap around Admin, Forms, and Serializers is a known limitation, but there may be patterns in your codebase that nobody's encountered yet. The tool gets better with more real-world cases.

How many fields are you sitting on that you haven't been able to delete?