How do you write and register a custom Django system check that verifies a third-party API key setting is present?
answer
- app_configs plus keyword arguments
- return a list, never raise
- Warning or Error with an id
- @register with a tag
- loaded from AppConfig.ready()
basics
~20 sWrite 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 sA 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# 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
Recall the pieces: a function returning a list of Error or Warning messages with ids, decorated with @register, in a module the app imports.
Explain the contract (app_configs plus keyword arguments), levels and ids, tags, deploy=True, and why the module must be imported from AppConfig.ready().
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.
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