How do you register a model with Django's admin, and how do you choose the columns its change list shows?
answer
- a module the admin discovers
- a function call or a class decorator
- a ModelAdmin subclass
- a tuple of column names
basics
~10 sRegister a model in the app's admin.py with admin.site.register(Ticket, TicketAdmin) or the @admin.register(Ticket) decorator on a ModelAdmin subclass. The change list shows the columns named in ModelAdmin.list_display, defaulting to the object's str.
solid answer
~40 sThe admin app autodiscovers each installed app's `admin.py`. In it you either call `admin.site.register(Ticket, TicketAdmin)` or decorate the `ModelAdmin` subclass with `@admin.register(Ticket)`; both register on the default site, and the decorator accepts `site=` for another one. Registering without a `ModelAdmin` gives the default options. The **change list** is the admin's per-model record page, and `list_display` chooses its columns: model field names, `__` paths to related fields (Django 5.1+), callables, `ModelAdmin` methods or model methods and properties. With no `list_display` you get a single column showing `__str__()`. A `ForeignKey` column shows the related object's `__str__()`, a boolean field shows yes/no icons, and `ManyToManyField` names are rejected because each row would need its own query.
code
python · 11 linesfrom django.contrib import admin
from helpdesk.models import Customer, Ticket
@admin.register(Ticket)
class TicketAdmin(admin.ModelAdmin):
list_display = ["subject", "status", "priority", "customer", "opened_at"]
admin.site.register(Customer) # default ModelAdmin optionsgo deeper
Know both registration forms, that they live in admin.py, and that list_display names the change list columns with str as the default.
Explain autodiscovery, the five kinds of list_display entries, and the rendering rules for foreign keys, booleans and many-to-many fields.
Show that you treat every list_display column as a query cost and keep registration consistent across several admin sites.
Be ready to argue which models deserve an admin at all, since each registration is an internal UI you must secure and maintain.
## Where registration happens `django.contrib.admin` ships with an app config whose `ready()` calls `admin.autodiscover()`. That imports the `admin` module of every installed app, so the convention is to register models in `<app>/admin.py`. A model that is never registered simply does not appear in the admin. There are two equivalent ways to register: 1. **`admin.site.register(Ticket, TicketAdmin)`** — a function call on the default `AdminSite` instance. The `ModelAdmin` class is optional; `admin.site.register(Ticket)` uses the default options. It also accepts an iterable of models. 2. **`@admin.register(Ticket)`** — a class decorator on the `ModelAdmin` subclass. It takes one or more models and an optional `site=` keyword for a custom `AdminSite`. It raises `ValueError` if no model is passed or if the decorated class does not subclass `ModelAdmin`. Both paths end in `AdminSite.register()`, which raises `AlreadyRegistered` if the model is registered twice on the same site and `ImproperlyConfigured` for an abstract model. ## The change list and list_display The **change list** is the page at `/admin/<app>/<model>/`: a paginated table of records with search, filters and actions around it. `list_display` decides its columns. Without it, the page shows one column, the object's `__str__()`, which is why a readable `__str__` matters. `list_display` accepts five kinds of entries: | Entry | Example | Notes | |---|---|---| | Model field name | `"subject"` | sortable by clicking the header | | Related field with `__` | `"customer__email"` | added in Django 5.1 | | Callable taking the instance | `ticket_age` | defined at module level | | `ModelAdmin` method name | `"sla_state"` | `def sla_state(self, obj)` | | Model method or property name | `"is_overdue"` | no required arguments | ## Built-in rendering rules - A **`ForeignKey`** column shows the related object's `__str__()`. - A **`BooleanField`** column shows yes/no/unknown icons instead of `True`/`False`/`None`. - **`ManyToManyField`** names are not accepted: rendering them would run a separate query per row. Use a method that builds the text, knowing it costs queries. - Empty values (`None`, empty string, empty iterable) render as `-`, which `empty_value_display` changes. - Output from methods and callables is **HTML-escaped**; build markup with `format_html()`. - The first column links to the change form unless `list_display_links` says otherwise. ## A support-ticket example For a help-desk app, a first useful change list shows the subject, status, priority, the customer and when the ticket was opened. Everything else (filters, search, computed columns) builds on this registration. ## Other options that shape the list A handful of neighbouring `ModelAdmin` attributes control how that table behaves: | Attribute | Default | Effect | |---|---|---| | `list_display_links` | `()` | which columns link to the change form; empty means the first column, `None` means no links | | `list_per_page` | `100` | rows per page | | `list_max_show_all` | `200` | the "Show all" link appears only when the total is at or below this | | `ordering` | `None` | default sort; falls back to the model's `Meta.ordering` | | `sortable_by` | `None` | which columns get clickable headers; `None` means all sortable ones | | `empty_value_display` | the site's `-` | placeholder for empty values | When the resulting order is not guaranteed to be total, the change list appends `-pk` so pagination never shows a row twice or skips one. For a ticket queue, `ordering = ["-opened_at"]` and a `list_per_page` of 50 are typical first adjustments. ## Mistakes to avoid - Registering in `models.py`: it works only if that module is imported early, and it mixes admin concerns into the model layer. Put it in `admin.py`. - Registering the same model twice, often once with the decorator and once with a stray `admin.site.register()` call, raises `AlreadyRegistered` at startup. - Listing a reverse relation or many-to-many field directly in `list_display` and expecting it to render. - Forgetting that every extra column may add work per row; related columns and methods can multiply queries, which is a separate tuning question.
- What does a Django admin change list show if list_display is not set?A single column containing each object's `__str__()`, linked to its change form. That is the default value of `list_display`, `("__str__",)`. It is why models that end up in the admin should define a meaningful `__str__`; otherwise staff see rows labelled like `Ticket object (42)`.
- Why does Django's admin reject a ManyToManyField name in list_display?Showing a many-to-many value would need a separate query for every row on the page. The system checks refuse the field name. If the column is really needed, write a method that joins the related names and pair it with `prefetch_related` in `get_queryset()`, so the page still runs a fixed number of queries.
saying these in an interview costs you the question
- Models appear in the admin automatically once they are migrated
- admin.site.register() requires a ModelAdmin class as its second argument
- list_display can show a ManyToManyField directly as a comma-separated column
- A ForeignKey column in list_display shows the related row's numeric id
- Registering a model twice on the same site just overrides the first registration