Chapter 21 · Management commands
21.4 The system checks
The system check framework is a set of functions, registered in a CheckRegistry, that examine a loaded project and return CheckMessage objects. CheckRegistry selects them by tag and calls them; BaseCommand sorts what they return by level, prints it, and raises SystemCheckError when a message is serious enough to stop the command.
Much of what can be wrong with a Django project is wrong before any request arrives: a field declared with an argument missing, two models claiming one table, a URL pattern that can never match, a setting of the wrong type. Python accepts all of it, because none of it is an error until the code that depends on it runs. The checks exist to find such faults early and say what they are. They look at the project as it stands once Django has started, its settings, its models, its URL patterns, its template engines, and do not exercise it.
Registered, run, reported
The framework keeps apart three things that are easy to run together.
Registering. A check is a function that takes the applications to look at and returns a list of messages. It becomes a system check by being registered, which puts it in a set, and that happens as Django starts: when a module is imported, or when an application is made ready.
Running. CheckRegistry picks functions out of the set, calls each, and returns all their messages in one list. It decides nothing about what the messages mean.
Reporting. BaseCommand is the only thing in Django that calls run_checks. It takes the list, leaves out the messages the project has silenced, prints the rest grouped by how serious they are, and raises an exception if any is serious enough.
The registry
CheckRegistry holds two sets of functions, registered_checks and deployment_checks. There is one registry in a process, made as its module is imported, and the names a project uses are that object’s methods: register, run_checks and tag_exists in django are bound to it.
CheckRegistry takes a function and any number of tags, strings that say what kind of check it is. It can be used as a decorator, with tags or bare, or called with the function first. It writes the tags onto the function and adds the function to the first set, or to the second if it was registered with deploy=True. It refuses a function that does not accept arbitrary keyword arguments, with a TypeError, so that a keyword argument can be added to what every check is called with and no registered check breaks.
Because the checks are kept in sets, they have no order. A check cannot count on another having run before it, and nothing a check does should matter to the next.
When a function is registered depends on where it lives.
- Django’s core checks are registered by importing
django. The package’s own module imports every module that defines them, and says in a comment that it does so to force registration. A process that has imported the model layer has them..core .checks - A contrib application’s checks are registered in its
AppConfig.ready, so only for a project that installs it. The admin, auth, contenttypes, sites and staticfiles applications each do this. - The check of the task backends is registered when
django.tasksis imported, so only in a process that imports that package. - A project’s checks follow one of these patterns: usually an application’s
readyimports the module that defines them.
This is the registry of the recorded project once Django has started. The project installs one contrib application, django, and one of its own applications, trees, registers a check under a tag of its own. The check looks at a setting, TREES_PER_ROW, and returns a warning when it is above 16 and an error when it is above 20.
The check registry after django.setup(), by the module that defines each check
registered_checks
django.contrib.staticfiles.checks tagged staticfiles
check_finders, check_storages
django.core.checks.caches tagged caches
check_default_cache_is_configured, check_file_based_cache_is_absolute
django.core.checks.commands tagged commands
migrate_and_makemigrations_autodetector
django.core.checks.compatibility.django_4_0 tagged compatibility
check_csrf_trusted_origins
django.core.checks.database tagged database
check_database_backends
django.core.checks.files tagged files
check_setting_file_upload_temp_dir
django.core.checks.mail tagged mail
check_mailers_default_alias
django.core.checks.model_checks tagged models
check_all_models, check_lazy_references
...
django.core.checks.urls tagged urls
check_custom_error_handlers, check_url_config, check_url_namespaces_unique,
check_url_settings
trees.checks tagged trees
check_trees_per_row
deployment_checks
django.core.checks.async_checks tagged async_support
check_async_unsafe
django.core.checks.caches tagged caches
check_cache_location_not_exposed
django.core.checks.mail tagged mail
check_mailers_production_backend
django.core.checks.security.base tagged security
Tags gives names to the tags of Django’s own checks, as attributes whose values are the same words. A tag is only a string, and a project invents its own by using it. A check may also have none, as the auth application’s check of the middleware has, and it is then run only when no tags are asked for.
One function, many objects
Most of the functions in the list look at a setting or two and are complete in themselves. A few are of another kind: they walk a collection of objects and ask each to check itself.
check_all_models is the plainest case. It goes through every model class, or those of the applications it was given, and calls the class’s Model.check. That method examines the model’s own options and asks each of its fields and managers, and each of its indexes and constraints, to check itself. What the function adds of its own is mainly what no single model can know: that two models use one table, or that an index or a constraint has a name another already has. The model checks goes through what Model.check asks.
The same shape recurs. check_url_config asks the root URL resolver, which asks every pattern under it. check_templates asks each configured template engine. check_database_backends asks each connection’s validation object. The admin’s check asks every admin site, which asks each model admin registered with it whose class is not the plain base class.
So a kind of object that can be misconfigured has a method named check, and some registered function reaches it. The knowledge of what makes a field or a pattern wrong stays with the field or the pattern, and the chapters on those subsystems are where their checks are told.
What run_checks selects
CheckRegistry takes four arguments: the applications to check, a collection of tags, whether to include the deployment checks, and a list of database aliases. All four may be left out.
- It starts from
registered_checks, withdeployment_checksadded if they were asked for. - If tags were given, it keeps the functions that carry at least one of them. If none were given and no databases either, it drops the functions tagged database: those checks, a comment says, do more than static analysis of code.
- If no databases were given, it takes every configured alias.
- It calls each remaining function with the applications and the databases as keyword arguments, and adds what the function returns to one list. A function that returns something that cannot be iterated over is a
TypeError.
The applications are passed through and nothing more. Whether a check narrows its work to them is the check’s business: the model check does, and a check of a setting has no reason to.
Step 2 means that the database checks have to be asked for, by a tag or by naming a database. A command that works on one database does the second: migrate overrides BaseCommand to pass its database, so that backend’s checks run before it migrates. It also means that a collection of tags that is given but empty selects nothing at all, which is how runserver keeps BaseCommand from running the checks it is about to run itself (The development server).
The checks of two tags, one of them the tag that is left out unless asked for, from BaseCommand.execute on
python manage.py check --database default --tag database --tag trees
...
Command.handle (django.core.management.commands.check)
BaseCommand.check(tags=["database", "trees"], display_num_errors=True, databases=["default"])
CheckRegistry.run_checks(tags=["database", "trees"], databases=["default"])
-> 2 check functions called; no messages
check_database_backends, check_trees_per_row
Messages and levels
What a check returns is a list of CheckMessage objects, and a message is five values: a level, the text, an optional hint, the object the message is about, and an id.
The level is a number, and five are named: DEBUG is 10, INFO 20, WARNING 30, ERROR 40 and CRITICAL 50, the scale of Python’s logging module. Five subclasses, Debug, Info, Warning, Error and Critical, each fix one, so that a check writes the class and not the number.
The id is a short string that names the kind of problem, by convention the area, a letter for the level and a number, as in "models.E028". It is what a project quotes to silence a message.
CheckMessage says whether its level reaches a given one, an error if none is given. CheckMessage says whether its id is listed in settings.
What BaseCommand.check does with them
BaseCommand calls run_checks and then decides what the result means for the command.
It sorts the messages that are not silenced into five groups by level, the most serious first, and builds a report: a header, then each group that has anything in it under its own heading. A message is shown as the object it concerns, or a question mark for none, then its id and text, with the hint on a line below. A caller may ask for a last line that counts the issues and the silenced messages; the check command and runserver do.
Then one test settles the outcome. If any message that is not silenced is serious at the fail level, which is an error unless the caller says otherwise, the method raises SystemCheckError with the report as its message, and the command goes no further: handle is never called. Otherwise the method prints the report and returns. A report that has issues in it goes to standard error; one that is only the count, with nothing found, goes to standard output.
A warning, then, is seen and does not stop anything.
A check returns a warning and the command runs, from BaseCommand.execute on
TREES_PER_ROW=18 python manage.py count 3
BaseCommand.execute(verbosity=1, settings=None, pythonpath=None, traceback=False, no_color=False, force_color=False, skip_checks=False, rows=3, per_row=None)
BaseCommand.get_check_kwargs
-> {}
BaseCommand.check()
CheckRegistry.run_checks()
-> 23 check functions called; trees.W001
OutputWrapper.write("System check identified some issues:\n\nWARNINGS:\n?: (trees.W001) TREE...") to stderr
Command.handle (trees.management.commands.count)
OutputWrapper.write("54 trees in 3 rows") to stdout
BaseConnectionHandler.close_all (the database connections)
stdout | 54 trees in 3 rows
stderr | System check identified some issues:
stderr |
stderr | WARNINGS:
stderr | ?: (trees.W001) TREES_PER_ROW is 18: the trees will be crowded.
exit status 0
An error stops the command before its own code runs. The exception passes out of execute, and BaseCommand catches it as it catches any CommandError, writes the report as it stands, and exits with status 1.
A check returns an error and the command does not run, from BaseCommand.run_from_argv on
TREES_PER_ROW=30 python manage.py count 3
...
BaseCommand.check()
CheckRegistry.run_checks()
-> 23 check functions called; trees.E001
OutputWrapper.write("SystemCheckError: System check identified some issues:\n\nERRORS:\n?: (...") to stderr
BaseConnectionHandler.close_all (the database connections)
stderr | SystemCheckError: System check identified some issues:
stderr |
stderr | ERRORS:
stderr | ?: (trees.E001) TREES_PER_ROW is 30, and a row has room for 20.
stderr | HINT: Plant another row.
exit status 1
Silencing is absolute. A silenced message is not printed and cannot stop the command, whatever its level; it survives only as a count in the last line of a report that prints one. Below, the same error is skipped by an option, then silenced, and a warning is made fatal by lowering the fail level. The recorded project copies the variable SILENCED into settings.
The same error skipped and silenced, and a warning made to fail
TREES_PER_ROW=30 python manage.py count 3 --skip-checks
stdout | 90 trees in 3 rows
exit status 0
TREES_PER_ROW=30 SILENCED=trees.E001 python manage.py count 3
stdout | 90 trees in 3 rows
exit status 0
TREES_PER_ROW=30 SILENCED=trees.E001 python manage.py check
stdout | System check identified no issues (1 silenced).
exit status 0
TREES_PER_ROW=18 python manage.py check --fail-level WARNING
stderr | SystemCheckError: System check identified some issues:
stderr |
stderr | WARNINGS:
stderr | ?: (trees.W001) TREES_PER_ROW is 18: the trees will be crowded.
stderr |
stderr | System check identified 1 issue (0 silenced).
exit status 1
When the checks run
The checks run when a command runs them, and at no other time.
--deploy.| who | when | which |
|---|---|---|
Base |
when before handle, for a command whose class asks for checks, unless the options say to skip them |
which every check but the database ones, or those with one of the tags the class lists |
| the check command | when in its handle |
which what its options say: applications, tags, --deploy, --database |
| migrate | when before handle, through execute |
which every check, the database ones for the database being migrated |
| runserver | when in the serving process, each time it starts | which every check but the database ones |
| the test runner | when after the test databases are made, by calling the check command | which every check, the database ones for the databases the tests use |
| a worker of a parallel test run | when as it starts, if it was not forked from the test process, by calling the check command with its output thrown away | which the same |
The check command is the framework’s own front. Its class asks execute for no checks and calls BaseCommand itself, so that its options can shape the run: app labels narrow the applications, --tag the tags, --database the aliases, --fail-level the level that fails, and --deploy adds the second set. The test runner’s two rows are calls of this command through call_command.
python manage.py check, from BaseCommand.run_from_argv on
python manage.py check
...
Command.handle (django.core.management.commands.check)
BaseCommand.check(display_num_errors=True)
CheckRegistry.run_checks()
-> 23 check functions called; no messages
check_all_models, check_csp_nonce_context_processor, check_csp_settings,
check_csrf_failure_view, check_csrf_trusted_origins, check_custom_error_handlers,
check_default_cache_is_configured, check_file_based_cache_is_absolute, check_finders,
check_language_settings_consistent, check_lazy_references, check_mailers_default_alias,
check_setting_file_upload_temp_dir, check_setting_language_code, check_setting_languages,
check_setting_languages_bidi, check_storages, check_templates, check_trees_per_row,
check_url_config, check_url_namespaces_unique, check_url_settings,
migrate_and_makemigrations_autodetector
and from inside them: import django.core.management.commands.makemigrations
and from inside them: import django.core.management.commands.migrate
and from inside them: import orchard.urls
and from inside them: import orchard.views
and from inside them: load_command_class("django.core", "makemigrations")
and from inside them: load_command_class("django.core", "migrate")
OutputWrapper.write("System check identified no issues (0 silenced).") to stdout
The last six lines under run_checks are imports and loads that happened inside the check functions, and they have consequences outside this chapter. The URL checks import the project’s root URLconf, and with it every module of views the URLconf imports. So any command that runs all the checks imports the URLconf and everything the URLconf imports, and an error raised by importing a view surfaces in a command that has nothing to do with views. And one check loads the migrate and makemigrations commands, to compare an attribute the two must share.
A server is not in the table. get_wsgi_application starts Django and makes a handler, and neither step calls the checks: a project deployed without ever running a command has never been checked. And call_command turns off the checks that execute would run, unless the caller asks for them (call_command).
The deployment checks
The functions in deployment_checks look for settings that are reasonable while developing and wrong in production: debugging left on, no allowed hosts, cookies not marked secure, the security middleware absent. Run by default they would complain about every development setup, so nothing runs them but the check command with --deploy. Most are the security checks, which belong to The security checks.
python manage.py check --deploy, for this project
python manage.py check --deploy
stderr | System check identified some issues:
stderr |
stderr | WARNINGS:
...
stderr | ?: (security.W018) You should not have DEBUG set to True in deployment.
stderr | ?: (security.W020) ALLOWED_HOSTS must not be empty in deployment.
stderr |
stderr | System check identified 6 issues (0 silenced).
exit status 0
All six here are warnings, so the command ends with status 0. A deployment script that wants warnings to stop it passes --fail-level WARNING as well.