django: custom UUID primary key on AbstractBaseUser subclass causes false positive bad-override-mutable-attribute
- Dominant language
- Rust
- Stars
- 7k
- Forks
- 516
- PR merge metrics
- No merged PRs in 30d
Description
## Description
pyrefly reports `bad-override-mutable-attribute` when a custom Django user model overrides `AbstractBaseUser`'s (and `PermissionsMixin`'s) implicit `id` field with a `UUIDField`. This is a very common, standard Django pattern (every Django project wanting UUID primary keys on its user model does this) and pyrefly's own docs say custom primary keys are correctly type-inferred - the false positive is specifically in the *override-consistency* check against the abstract base classes, not in resolving the concrete field's own type.
## Reproduction
```python
import uuid
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
class User(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
```
### Actual behavior
```
ERROR Class member `User.id` overrides parent class `AbstractBaseUser` in an inconsistent manner [bad-override-mutable-attribute]
|
| id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
| ^^
|
`User.id` has type `UUID`, which is not consistent with `int` in `AbstractBaseUser.id` (the type of read-write attributes cannot be changed)
ERROR Class member `User.id` overrides parent class `PermissionsMixin` in an inconsistent manner [bad-override-mutable-attribute]
```
### Expected behavior
No error. Per your own docs (https://pyrefly.org/en/docs/django/): "if you define a field with primary_key=True, Django will not add the id field" - the framework's own actual runtime semantics never give `AbstractBaseUser`/`PermissionsMixin` an independent `int` id at all; only the final concrete model has a real pk, and it's the one this class declares.
## Why this is a false positive
`AbstractBaseUser`/`PermissionsMixin` are abstract base classes (`class Meta: abstract = True` in Django terms). Django's implicit `id = AutoField(primary_key=True)` injection only ever applies to the final concrete model in an inheritance chain if nothing in that chain declares its own pk - it is not a real, independent field that exists on the abstract parent classes themselves. pyrefly appears to synthesize an implicit `int` id specifically on the *abstract* base classes (since they don't declare a pk field individually), then flags the concrete subclass's real, intentional `UUIDField(primary_key=True)` as an incompatible override of that synthesized-but-never-actually-instantiated attribute.
I tried the officially-suggested approach of explicitly parameterizing the field's generic type (`id: models.UUIDField[uuid.UUID | str, uuid.UUID] = models.UUIDField(...)`), which did not resolve it either (tested on pyrefly 1.2.0).
## Real-world impact
Affects any Django project using a custom `AUTH_USER_MODEL` with a non-integer (UUID, ULID, etc.) primary key while inheriting from `AbstractBaseUser`/`PermissionsMixin` - a widely-recommended pattern for new Django projects, especially anything exposing IDs over an API.
## Workaround
Can suppress with `bad-override-mutable-attribute = "ignore"` in `[tool.pyrefly.errors]`, but that's a blanket suppression that would also hide genuine mutable-attribute override bugs elsewhere in the codebase.
## Environment
- pyrefly version: 1.2.0
- django-stubs version: 6.1.0
- Python: 3.14
Contributor guide
Research direction
Start with the Django reproduction in the issue and the documented custom-primary-key behavior. Trace the override-consistency check for the synthesized `id` on `AbstractBaseUser` and `PermissionsMixin`, then verify the fix against the shown output. Done means the UUID primary-key model reports no false-positive override errors while genuine mutable-attribute overrides remain checked.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- django, python
- Domain
- authentication, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 65/100