DeepSeek can write useful code changes, but you still have to run your automated tests before you trust one. A tidy-looking fix can quietly remove a safety check, such as the filter that stops one customer account (a “tenant”) from seeing another account’s records.
Imagine your company’s invoice page is slow, so you paste the code behind it into DeepSeek and ask it to speed things up without changing anything else. The answer looks clean, and the page does load faster. But when you refresh it, a customer who should see 3 invoices now sees 4,815, because the new code dropped the one line that limited the list to that customer’s own records.
The 4,812 rows that were not our tenant
Tenant 17 is Acme Print, and three open invoices sat on the list: INV-17-088, INV-17-089, and INV-17-090. The page had been slow because the template printed invoice.customer.name and invoice.job.title without a join (a way to fetch the related records in one go), so Django (the web framework the app is built on) fired a separate query (a request to the database) for every single row. You want that lag gone before standup, and you do not want a new product architecture, just select_related.
The function you pasted was 11 lines:
class InvoiceListView(ListView):
model = Invoice
paginate_by = 25
def get_queryset(self):
return (
Invoice.objects.filter(tenant_id=self.request.tenant.id)
.order_by("-issued_on")
)TenantMiddleware in tenants/middleware.py sets request.tenant based on the host. Acme Print is acme-print.ledger.example, Harbor Books is a different host, and Birch Gym is a different host again. The filter is the product: without it, the list becomes every shop on the box at once.
Expert Mode rewrote the method like this:
def get_queryset(self):
return (
Invoice.objects.select_related("customer", "job")
.order_by("-issued_on")
)The N+1 problem is gone, meaning Django no longer fires one query per row, but the tenant filter is gone too. The page dropped from about 1.8 seconds to about 0.4 seconds locally, which is exactly why you almost shipped it. A quick Slack message was already drafted: preview looks fast. Then you looked at the header, and {{ paginator.count }} invoices said 4,815. Page 1 still showed 25 rows, because paginate_by is 25, but row 4 was INV-04-221 from Birch Gym and row 7 was INV-31-009 from Harbor Books. Acme Print’s three invoices were in the mix, and so were 4,812 invoices that belong to other tenants entirely.
The toy counts for this post:
| Check | Friday afternoon (quick fix branch) | After the Expert rewrite |
|---|---|---|
filter(tenant_id=...) in get_queryset | present | missing |
| Invoices for tenant 17 (Acme Print) | 3 | 3, plus everyone else |
paginator.count on /app/invoices/ | 3 | 4,815 |
| Extra rows from other tenants | 0 | 4,812 |
test_invoice_list_stays_on_tenant | would pass | fails (count 4815, expected 3) |
There was no test for this. The file invoices/tests/test_tenant_scope.py did not even exist, and the suite that did exist only checked login and the PDF download button. Nobody had ever asserted that a list view stays inside request.tenant.id, so Expert Mode could not fail a test that was never written. The chat also cannot see your database: it rewrote 11 lines of Python against a prompt that said keep behavior and never once mentioned tenants.
Hangzhou-based DeepSeek markets itself heavily around agentic coding, where the AI writes and runs code on its own, step by step. The lab’s own preview post for V4 called V4-Pro strong on agentic coding benchmarks (standard tests used to compare AI models, like an exam every model takes), and later release notes describe agent scores on tests such as Terminal Bench (tasks done by typing commands) and DeepSWE. The V4-Pro model card listed, as of October 2026, SWE-bench Verified around 80.6 resolved and SWE-bench Pro around 55.4 under the lab’s own reported settings, though you should copy the live card the week you cite those figures, since scores like that move. None of those numbers are InvoiceListView. A score on a public patch set does not keep Birch Gym’s invoices off Acme Print’s screen.
Rule of thumb: if
tenant_idleft the queryset, the page is not done, even when it got faster.
What coding help is actually good for
DeepSeek is genuinely useful for coding, and that sentence can sit right next to 4,812 extra rows from other tenants without any contradiction. The hang-up is always the job you give it. Four jobs fit comfortably into a Friday afternoon, and a fifth job, dumping production into the chat tab, does not.

Rewrite a function when you already know the contract. The contract here was “Acme Print sees Acme Print,” so a better prompt would have pasted the 11 lines, named the filter directly, and asked only for select_related on customer and job. You can also ask it to split a 60-line helper, rename a confusing argument, or turn a nested loop into a dict lookup. Either way, you read the diff yourself, and you never merge just because the comment in the chat sounds confident.
Explain an error when you have a traceback and the five lines that threw it, something like DoesNotExist on Invoice.objects.get(pk=pk) after a tenant was deleted, or an IntegrityError on a unique (tenant_id, number) pair, or a 500 on /app/invoices/ because job was null and the template assumed a title. Paste the stack, redact hostnames and customer emails, and ask what to check, then go check it yourself. The model will invent a missing migration if you let it, so your own showmigrations output is the ground truth, not its guess.
Write a test when you can name the invariant in one sentence. For this list, the invariant is: a user in tenant 17 must see 3 invoices, never 4,815. Ask Expert Mode, or the API (the way your own programs send DeepSeek a request without the chat window), to draft test_invoice_list_stays_on_tenant, then run it yourself. The next section walks through that exact test, including the easy mistake of asserting page length instead of paginator.count.
Do not dump production data into the chat. If .env on your local machine points at a shared database dump, a multi-tenant leak becomes dangerous fast. A pg_dump of invoices is other people’s money, and a CSV export of 4,815 live rows is a privacy incident wearing a “need more context” costume. The model does not get smarter because you pasted the production database; it just gets your secrets. Chat and the API both leave your laptop the moment you hit send, and where that prompt ends up living is covered in a separate post on hosted access and privacy. The coding rule stays simple: toy fixtures are enough.
Instant Mode would have been a cheaper, faster guess for a job this small, but people often pick Expert simply because the work feels serious. Expert Mode is currently V4-Pro on the consumer chat, and Instant is V4-Flash. Neither mode runs Django, and neither mode opens tenants/middleware.py unless you paste it in yourself. Thinking traces, if you left thinking on, are still only a scratch pad, as an earlier post in this series covered in detail. A long reasoning block that says “we preserve existing filters” is not the same thing as a grep of tenant_id in the new method.
The lab’s agent story is real enough to try. Official docs show how to point Claude Code, OpenCode, OpenClaw, and GitHub’s Copilot command-line interface (CLI) at DeepSeek’s API. Fill-in-middle completion (FIM) also exists on the API as a non-thinking-only surface, which is different from a standard chat tab. None of those tools replace pytest. If you arrived here from a screenshot of a benchmark score, keep that screenshot in a bookmarks folder, and put the tenant test in the repo instead.
Paste a function, not the secrets file
Pasting one method rather than the entire file is good practice on its own, but leaving out the multi-tenant isolation rule lets the model produce code that reads like a generic select_related tutorial, one that drops tenant isolation without any warning.
A paste that would have actually helped:
- The current
get_queryset, includingfilter(tenant_id=self.request.tenant.id). - One sentence of constraint: never drop that filter, because Acme Print must not see other tenants.
- The template loop that touches
customerandjob, soselect_relatedhas a clear reason to exist. - The failing test, if you already have one, or at minimum the sentence “paginator.count must stay 3 for tenant 17.”
A paste that would have made Friday worse:
.env.prodand.env.local. Keys do not debug an N+1 problem.prod.sqlor apg_dumpofinvoices_invoice. That is 4,815 live rows plus every other table you did not mean to include.- A screenshot of the Django admin with other tenants visible, because the testing session happened to use a superuser account.
- Customer emails from Harbor Books “so the model understands the domain.”
If you would not email the file to DeepSeek support directly, do not paste it into Expert Mode either. The consumer chat is hosted, and the API is hosted too, both run by the same lab, Hangzhou-based DeepSeek. A related post maps the different doors: the chat site, the API, Hugging Face files, and other cloud hosts. This page only needs the paste test. Toy data is allowed, and it should be labeled as such. Three Acme invoices and 4,812 fake other-tenant rows in a pytest fixture are teaching text. Live Stripe keys are not.
Agents change the volume, not the rule. Claude Code pointed at https://api.deepseek.com/anthropic can read more of the repo than you intended, if you leave the working tree open, which is convenient for a refactor but is also how .env.prod ends up in a prompt without you ever hitting paste yourself. Put secrets in gitignore, load them from the environment, and keep a tiny fixture module ready for the model to use instead. If the agent needs the schema (the layout of your data: which tables exist and what columns they hold), paste the model fields for Invoice, never the live database.
Bounded paste also keeps you honest about what the model can actually know. It cannot know that request.tenant is required unless the method, the middleware (the code that runs on every request before your view sees it), or your prompt says so directly. It cannot know that paginate_by = 25 hides a 4,815 count on page 1 unless you mention the header yourself, and it cannot know Birch Gym is a different customer at all. You have to say the invariant out loud, and then you have to test it.
Run the test that would have caught tenant_id
The loop is short on purpose: paste, patch, run pytest, then ship. Skip that third step and you get an accidental multi-tenant leak instead.

Write the test before you enjoy the 0.4-second page. pytest-django gives you a test client and the @pytest.mark.django_db decorator. Fixtures build tenant 17 with three invoices and a second tenant with 4,812 invoices, then you log in as Acme Print’s owner, send a GET request to /app/invoices/, and assert the paginator count rather than just the rows visible on page 1.
import pytest
from invoices.models import Invoice
@pytest.mark.django_db
def test_invoice_list_stays_on_tenant(client, tenant_acme, tenant_other):
Invoice.objects.bulk_create(
[Invoice(tenant=tenant_acme, number=f"INV-17-{n:03d}") for n in (88, 89, 90)]
)
Invoice.objects.bulk_create(
[Invoice(tenant=tenant_other, number=f"INV-X-{i:04d}") for i in range(4812)]
)
client.force_login(tenant_acme.owner)
response = client.get("/app/invoices/")
assert response.status_code == 200
paginator = response.context["paginator"]
rows = response.context["object_list"]
assert paginator.count == 3
assert all(inv.tenant_id == tenant_acme.id for inv in rows)After Expert Mode’s rewrite, that test fails loudly: paginator.count comes back 4,815 where 3 was expected, and those 4,812 extra rows are the other tenant. If you had written assert len(rows) == 3 instead, and Acme Print later grew past 25 invoices, page 1 would still be length 25 whether or not a leak existed, and the assertion would quietly go stale. paginator.count is the number the header actually prints, so match the header.
A second assertion is worth adding once the count check turns green: confirm that every inv.tenant_id on the page equals tenant_acme.id. The count alone can lie if you later add a staff bypass that returns Invoice.objects.all() for superusers, and you happen to run the test as a superuser by accident. Your owner fixture should stay a normal tenant user. Staff preview deserves its own separate test with its own login.
Run it on your machine:
pytest invoices/tests/test_tenant_scope.py -qA red result means the filter is gone, or the login is wrong, or the URL changed, so do not ask the chat whether the test passed, because the chat never started Django in the first place. An agent that runs pytest inside your repo is closer to the truth, but you still read the failure yourself. Restore the line .filter(tenant_id=self.request.tenant.id) in front of select_related, run the test again, and confirm you see 3, not 4,815. Only then is the speed win allowed to stay.
The patched method that should have come back:
def get_queryset(self):
return (
Invoice.objects.filter(tenant_id=self.request.tenant.id)
.select_related("customer", "job")
.order_by("-issued_on")
)Same join, same order, tenant stays. That is the whole coding lesson in one method: DeepSeek can draft it, and you can draft it, but the test is what actually decides. If your app is not Django, the shape still holds, because it is always one invariant, one failing test, and one filter you can grep for. A FastAPI dependency that reads tenant_id from a signed login token (JWT) is the same story with different names. Skip the test, and you will eventually meet somebody else’s rows in a preview.
API backends vs the chat tab
The chat tab is often the right first door for an 11-line function: you paste, copy a patch, and run pytest without any extra tooling. Consumer web and app chat is currently free, with fair-use throttling possible, and there is no official DeepSeek Plus tier in the ChatGPT Plus shape. Clone checkouts are never the real lab. Expert Mode is the heavy model, and Instant Mode is the fast one, so confirm those exact labels the week you open the site, since they do shift.
The API is a different handle on the same hosted models. The base URL in OpenAI’s format is https://api.deepseek.com, and in Anthropic’s format it is https://api.deepseek.com/anthropic. Keys live on platform.deepseek.com, and current model ids include deepseek-v4-flash, deepseek-v4-pro, and an experimental deepseek-v4-flash-vision-exp. Peak and off-peak token rates sit on the live pricing page, so copy numbers from there rather than from a blog post. New API accounts may come with a free token grant, though that is also worth double-checking rather than assuming.
Agent integrations are documented directly by the lab. Claude Code is Anthropic’s terminal coding assistant, and DeepSeek’s own guide sets ANTHROPIC_BASE_URL to the Anthropic-compatible endpoint, pointing the Opus and Sonnet-class slots at deepseek-v4-pro and the Haiku-class slot at deepseek-v4-flash. Some of the example configs on that page use a [1m] suffix on the Pro id, so copy the live snippet the morning you wire this up rather than trusting an old copy. OpenCode has a /connect flow that lists DeepSeek as a provider. Copilot CLI supports a bring-your-own-key setup (BYOK) through that same Anthropic-compatible base URL, and OpenClaw can take a DeepSeek key during onboarding. Mentioning these tools here is educational identification only, not a claim of partnership with any of them.
A minimal Claude Code setup, trimmed from the docs, looks like this. Put the real key in your shell environment, never in the chat transcript itself:
export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
export ANTHROPIC_AUTH_TOKEN=YOUR_DEEPSEEK_KEY
export ANTHROPIC_MODEL=deepseek-v4-pro
export ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-v4-pro
export ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-v4-pro
export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash
export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash
export CLAUDE_CODE_EFFORT_LEVEL=maxThe agent can edit invoices/views.py and run pytest if you let it, which is the real upgrade from a manual chat tab, but it is also a faster way to drop tenant_id across three list views instead of just one. Same invariant, same test: if test_invoice_list_stays_on_tenant comes back red, you do not merge, and if the agent claims “all tests passed” and you cannot find that output in your own terminal, you run it again yourself.
Pick the chat tab when the change is one function you can see in full. Pick the API plus an agent when you are already inside a repo and want the tool to open files itself, with the same tenant test running in continuous integration (CI), the automated pipeline that runs your test suite on every change. Pick neither when the work is “show me every invoice in production so you understand,” because that is simply a data dump. Hosted access versus open weights, including pulling a DeepSeek tag through Ollama, is covered in the next post in this series, and privacy, region, and work caution get their own post as well. For Monday, add test_invoice_list_stays_on_tenant, restore the filter, and keep the select_related. Then read hosted DeepSeek vs open weights before you treat a local pull as the same thing as the chat site.
Series notes
This is Part 4 of Learn DeepSeek. Previous: thinking mode. Next: hosted vs open weights.
Sources
Product pages, API docs, and AMS paths used for this article. Confirm model ids, mode labels, prices, and bench tables the week you draft; this list was checked in September 2026.
- DeepSeek (Hangzhou lab site)
- DeepSeek chat (consumer web chat, Instant and Expert toggles)
- DeepSeek platform (API keys)
- DeepSeek API docs: Integrate with AI tools (Claude Code, OpenCode, OpenClaw)
- DeepSeek API docs: GitHub Copilot CLI (BYOK via the Anthropic-compatible endpoint)
- DeepSeek API docs: Models and pricing (ids, FIM note, peak windows; copy live numbers)
- DeepSeek API docs: V4 preview (24 Apr 2026, Expert / Instant, agentic coding pitch)
- DeepSeek API docs: change log (V4-Pro general release 13 Aug 2026, agent numbers, thinking effort)
- Hugging Face: deepseek-ai and the DeepSeek-V4 collection (open weights; SWE-bench figures on the card, hedge)
- DeepSeek-V4-Pro model card (lab-reported benches, including SWE Verified / SWE Pro, checked October 2026; not your queryset)
- AMS: DeepSeek from scratch · DS1 · DS3 · DS5
- AMS: Open-source AI explained · OS1 · OS2 · Learn · chooser
Keep going
Same lessons in your feed
Short diagrams, hooks, and weekly tutorials on Substack, Instagram, X, and Facebook.
