skip to content

System Checks

The system check framework validates models, settings and URLs before runserver and migrate, and check --deploy flags unsafe settings. Interviewers ask how to register a check of your own.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

5

How do you write and register a custom Django system check that verifies a third-party API key setting is present?

level: middleimportance: must knowfreq 50%

answer

  1. app_configs plus keyword arguments
  2. return a list, never raise
  3. Warning or Error with an id
  4. @register with a tag
  5. loaded from AppConfig.ready()

basics

~20 s

Write a function taking app_configs plus keyword arguments that returns a list of django.core.checks messages, such as Error(..., id="shipping.E001"), when the setting is missing; decorate it with @register and import its module from AppConfig.ready() so it registers.

solid answer

~40 s

A check is a function with the signature `(app_configs, **kwargs)` that returns a **list** of messages from `django.core.checks` — empty when all is well, otherwise `Warning`, `Error` or `Critical` objects with a short `msg`, an optional `hint` and a unique `id` like `shipping.E001`. Decorate it with `@register()` plus a tag (a built-in `Tags.*` value or your own string such as `"shipping"`), and add `deploy=True` if it only makes sense against production settings. Registration happens at import time, so put the checks in `shipping/checks.py` and import that module from `ShippingConfig.ready()`. A useful split for an API key: a `Warning` in every environment and a deployment-only `Error` so `check --deploy` blocks a release that forgot the key — and never echo the key's value in the message.

code

python · 27 lines
python
# shipping/checks.py
from django.conf import settings
from django.core.checks import Error, Warning, register


def _has_key():
    return bool(getattr(settings, "SHIPPING_API_KEY", ""))


@register("shipping")
def check_shipping_api_key(app_configs, **kwargs):
    if _has_key():
        return []
    return [
        Warning(
            "SHIPPING_API_KEY is not set; label printing is disabled.",
            hint="Set SHIPPING_API_KEY in this environment's settings.",
            id="shipping.W001",
        )
    ]


@register("shipping", deploy=True)
def check_shipping_api_key_deploy(app_configs, **kwargs):
    if _has_key():
        return []
    return [Error("SHIPPING_API_KEY must be set in deployment.", id="shipping.E001")]

go deeper

for a junior

Recall the pieces: a function returning a list of Error or Warning messages with ids, decorated with @register, in a module the app imports.

for a middle

Explain the contract (app_configs plus keyword arguments), levels and ids, tags, deploy=True, and why the module must be imported from AppConfig.ready().

for a senior

Show judgement on what belongs in a check: static presence and shape only, no network calls, no secrets in output, warnings versus deploy-only errors per environment.

for a principal

Use custom checks as a shared contract across teams — stable id namespaces per app and a rule for which conditions block releases.

## The shape of a check A **system check** is an ordinary function registered with Django's check registry. The contract is small and strict: - It accepts **`app_configs`** and **`**kwargs`**. `app_configs` is `None` when the whole project is checked, or a list of `AppConfig` objects when someone runs `manage.py check shipping`. The `**kwargs` is mandatory — `register` raises `TypeError` for a function that cannot accept keyword arguments — and currently carries a `databases` list. - It **returns a list** of messages. An empty list means "no problems". Returning `None` or a single message makes the registry raise `TypeError`. - It **does not raise** to report a problem. An exception inside a check is not converted into a message; it crashes whatever command was running. ## Messages Messages are instances of `CheckMessage`, usually through the level classes: | Class | Level | Effect | |---|---|---| | `Debug`, `Info` | 10, 20 | Printed only | | `Warning` | 30 | Printed; command continues | | `Error` | 40 | Command stops with `SystemCheckError` | | `Critical` | 50 | Command stops with `SystemCheckError` | Each takes `msg` (a short, single-line description), an optional `hint` (one line on how to fix it), an optional `obj` (the object at fault, printed as a prefix) and an **`id`**. Ids follow `applabel.X001`, where `X` is `C`, `E`, `W`, `I` or `D` for the level; they are what `SILENCED_SYSTEM_CHECKS` and your tests refer to, so never reuse or renumber them. ## Registering it `register` works as a decorator or a function: 1. `@register()` — no tags; the check runs whenever checks run. 2. `@register("shipping")` or `@register(Tags.security)` — tags let people run it with `check --tag shipping`; custom string tags are fine. 3. `@register("shipping", deploy=True)` — a **deployment check**, run only by `check --deploy`. 4. `register(check_function, "shipping")` — the non-decorator form. Registration is a side effect of **importing** the module. The docs recommend a module that is loaded when the app is loaded, typically imported from `AppConfig.ready()`. A check defined in a module nothing imports simply never runs, and nothing warns you. ## Designing the API-key check For a setting such as `SHIPPING_API_KEY`, decide what "missing" should mean in each environment: - In **development**, a missing key might only disable label printing — a `Warning` that reminds developers without blocking `runserver`. - In **deployment**, a missing key breaks checkout — an `Error` registered with `deploy=True`, so the release gate (`check --deploy`) fails. - Check **presence and shape** only (non-empty, maybe the expected prefix). A check must not call the provider's API: checks run before most commands, in CI and on developer laptops, and a network call would make every command slow and flaky. - Never put the key's value in `msg` or `hint`; check output ends up in CI logs. Whether a settings-level check should honour `app_configs` is a judgement call. A check that validates one app's settings can return `[]` when `app_configs` is given and does not include that app, so `check orders` stays focused. ## Testing it Messages implement equality on level, message, hint, object and id, so a unit test can call the function directly and compare the list it returns. An integration test can run `call_command("check")` and assert that `SystemCheckError` is raised for an `Error`, or capture `stderr` for a `Warning`. ## Common mistakes - **Reading settings at import time** in `checks.py` (`KEY = settings.SHIPPING_API_KEY` at module level) instead of inside the function, which freezes the value and breaks tests that change settings. - **Missing ids**, which makes the message impossible to silence or to assert on precisely. - **Multi-line or very long messages.** The reference asks for a short, single-line `msg` (under 80 characters) and a single-line `hint`. - **Using `Critical` for everything.** `Error` already stops commands; `Critical` is for problems that make the project unusable. - **Returning a message instead of a list** (`return Warning(...)`), which makes the registry raise `TypeError` the first time the check runs. ## Alternatives to a registered function Fields, models, managers, constraints, template engines, task backends and database backends already have a `check()` method wired into the framework. When the rule is about a custom field or model, extend that `check()` and append your messages instead of registering a new function.

  • How do you unit-test the check without running the whole check command?
    Call the function directly with `app_configs=None` under the settings you want (for example with `override_settings`) and compare the returned list. `CheckMessage` defines equality on level, msg, hint, obj and id, so `self.assertEqual(check_shipping_api_key(None), [Warning(...)])` works, or assert on the ids alone for less brittle tests.
  • Your new check never reports anything, even with the setting removed; what do you look at first?
    Whether its module is imported at all: registration is an import side effect, so a `checks.py` that neither `AppConfig.ready()` nor anything else imports never registers. Then confirm the app is in `INSTALLED_APPS` with the config that defines `ready()`, and that you ran `check --deploy` if the check was registered with `deploy=True`.

saying these in an interview costs you the question

  • A check signals a problem by raising an exception
  • Registering a check needs an entry in a CHECKS setting
  • Checks should ping the third-party API to prove the key works
  • A check function does not need to accept keyword arguments
  • Message ids are cosmetic and can be renumbered freely
open as a page

What does Django's manage.py check --deploy add, and how do you make it fail a CI build on deployment warnings?

level: middleimportance: must knowfreq 55%

basics

~20 s

check --deploy adds checks registered with deploy=True, mostly security.* warnings such as W004, W018 and W020. They are warnings, so the default exit status stays 0; run it with production settings and --fail-level WARNING to fail CI.

open as a page

What is Django's system check framework, and when do its checks run without anyone calling manage.py check?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Django's system check framework (django.core.checks) is a registry of static validations over models, settings, URLs and templates. Besides manage.py check, it runs before most management commands, including runserver and migrate, but never inside the deployed request path.

open as a page

In Django's system check framework, how do message levels and ids like security.W004 work, and what does SILENCED_SYSTEM_CHECKS suppress?

level: middleimportance: should knowfreq 35%

basics

~20 s

Messages carry a level (Debug to Critical); Error and Critical stop management commands, lower levels are printed. Ids follow applabel.X001, where X marks the level. SILENCED_SYSTEM_CHECKS lists ids whose messages are hidden and excluded from the pass/fail decision.

open as a page

A custom Django system check that queries a shop table breaks migrate on a fresh database and every command in CI; what is wrong and how do you fix it?

level: seniorimportance: should knowfreq 26%

basics

~20 s

An untagged check runs before nearly every command, so its query fails wherever no database or table exists, and migrate runs checks before migrating. Keep data invariants out of checks; tag real database checks with Tags.database and use only the aliases passed in.

open as a page