In Django, which permission-string mistakes make user.has_perm() silently return False, and how does Permission.user_perm_str in 6.1 help?
answer
- membership test, not a lookup
- label, not module path
- lowercase model in codename
- superusers mask typos
- a property on the row
basics
~20 shas_perm() only tests whether an 'app_label.codename' string is in the user's set, so a missing app label, a module path, a capitalised model name or a typo just returns False. Django 6.1's Permission.user_perm_str builds the correct string from a Permission row.
solid answer
~40 s`has_perm()` never validates that a permission exists: `ModelBackend` checks whether the exact `"app_label.codename"` string is in the user's set, so a wrong string is simply `False`. Common mistakes are dropping the app label, using the module or package path instead of `AppConfig.label`, writing the model name in capitals, or using a model-then-action shape. Active superusers get `True` for any string, which hides typos in manual testing, while `has_perms()` given a bare string raises `ValueError`. Django 6.1's `Permission.user_perm_str` returns `app_label.codename` from a row, so code that already holds `Permission` objects stops hand-assembling strings; `get_permission_codename()` and a constant per custom permission do the same for code checks.
go deeper
Know the exact string shape — app label, dot, lowercase codename — and that a wrong one quietly returns False rather than raising.
Explain the membership test in ModelBackend, where the app label comes from, and why superuser testing hides mistakes.
Remove hand-typed strings: derive them with user_perm_str or get_permission_codename, centralise constants, and add a test that every checked string exists.
Treat permission strings as a contract across views, templates and APIs, and decide how that contract is named, owned and verified.
## Why a wrong string fails quietly `user.has_perm(perm)` does not look up a `Permission` row by name. For a normal user, `ModelBackend` builds a set of strings of the form `"<app_label>.<codename>"` and checks **membership**. A string that matches nothing is not an error — it is simply not in the set, so the answer is `False`. Django never validates that the permission exists. That makes typos and format mistakes invisible until someone notices a feature that nobody can use. ## The usual mistakes For an app labelled `newsroom` with an `Article` model and a custom permission `publish_article`, the correct string is `"newsroom.publish_article"`. These all return `False` for a user who holds it: | String | What is wrong | |---|---| | `"publish_article"` | No app label | | `"newsroom.Article.publish"` | Model-then-action shape; Django uses app label plus codename | | `"newsroom.models.publish_article"` | Module path instead of app label | | `"newsroom.change_Article"` | Codenames use the lowercase model name | | `"apps.newsroom.publish_article"` | Full package path; the app label is `AppConfig.label`, by default only the last component | | `"news.publish_article"` | Guessing the label when `AppConfig.label` was customised | Two further traps sit next to these: - **Superusers hide typos.** `PermissionsMixin.has_perm()` returns `True` for any string when the user is an active superuser, so a misspelled permission passes every manual test done from a superuser account. - **`has_perms()` with a bare string** is loud instead: it raises `ValueError("perm_list must be an iterable of permissions.")` rather than iterating the characters. ## Building the string instead of typing it Hand-typed strings drift from the model. Django gives you ways to derive them: 1. **`Permission.user_perm_str` (Django 6.1).** A property on a `Permission` row that returns `f"{content_type.app_label}.{codename}"` — exactly the string `has_perm()` wants. It is useful whenever you already hold `Permission` objects, for example when listing the rights a role grants or asserting on them in tests. 2. **`django.contrib.auth.get_permission_codename(action, opts)`.** Returns the default codename for an action and a model's `_meta`, such as `change_article`; combine it with `opts.app_label` to build the full string for the built-in actions. The admin builds its checks this way. 3. **A constant per custom permission.** Define `PUBLISH_ARTICLE = "newsroom.publish_article"` once, next to the model, and import it wherever it is checked. ## A test that catches a dead string Because a wrong string fails silently, a cheap guard is a test that asserts every permission string your code checks corresponds to a real row: ```python from django.contrib.auth.models import Permission from django.test import TestCase CHECKED_PERMS = {"newsroom.publish_article", "newsroom.change_article"} class PermissionStringsTests(TestCase): def test_every_checked_permission_exists(self): existing = {p.user_perm_str for p in Permission.objects.all()} self.assertEqual(CHECKED_PERMS - existing, set()) ``` In Django's test runner the test database is built by running migrations, so the `post_migrate` handler has created the permission rows before the test runs. ## Diagnosing a False step by step When a user reports that a feature is denied although "they have the permission", work through the chain in order: 1. **Is the user active?** `ModelBackend` returns `False` for every check on an inactive user. 2. **What does the user actually hold?** Load a new instance and print `user.get_all_permissions()`; compare the strings character by character with the one in the code. 3. **What is the real app label?** Query `Permission.objects.filter(codename="publish_article").values_list("content_type__app_label", flat=True)` to see the label the row lives under. 4. **Is the check running on a stale object?** If the grant happened earlier in the same request or process, the instance may be answering from its cached set. 5. **Is another backend involved?** `has_perm()` asks every entry in `AUTHENTICATION_BACKENDS`; a custom backend can raise `PermissionDenied` and stop the loop with `False`. ## What the string cannot express - The string holds only the app label and the codename, not the model. Two models in one app that both declare a codename `publish` produce the same string, so a codename should include the model name. - The string names a model-wide right. Per-object answers ("this article") come from other backends and the `obj` argument, which the built-in `ModelBackend` answers with an empty set.
- Why can a typo pass a manual check in development but fail for real editors?Developers often test with a superuser. `PermissionsMixin.has_perm()` returns `True` for an active superuser before any backend runs, whatever the string says, so a misspelled permission looks fine. For an editor the backend tests set membership, and the typo never matches.
- Where does the app label in the string come from if the app lives at apps/newsroom?From `AppConfig.label`, which defaults to the last component of the app's module path — `newsroom` for `apps.newsroom`. If the `AppConfig` sets a different `label`, that label is what the string must use.
saying these in an interview costs you the question
- has_perm() raises an error when the permission does not exist
- The app label is the dotted package path of the app
- A superuser account is a good way to test permission strings
- has_perms('newsroom.publish_article') checks that single permission