Django with mypy: How to resolve incompatible types error due to redefined field for custom `User` model class that extends "AbstractUser"?
Asked Answered
W

2

6

I have an existing Django project which uses a custom User model class that extends AbstractUser. For various important reasons, we need to redefine the email field as follows:

class User(AbstractUser):
    ...
    email = models.EmailField(db_index=True, blank=True, null=True, unique=True)
    ...

Typing checks via mypy have been recently added. However, when I perform the mypy check, I get the following error:

error: Incompatible types in assignment (expression has type "EmailField[str | int | Combinable | None, str | None]", base class "AbstractUser" defined the type as "EmailField[str | int | Combinable, str]") [assignment]

How can I make it so that mypy allows this type reassignment? I don't wish to just use # type: ignore because I wish to use its type protections.

For context, if I do use # type: ignore, then I get dozens of instances of the following mypy error instead from all over my codebase:

error: Cannot determine type of "email" [has-type]

Here are details of my setup:

python version: 3.10.5
django version: 3.2.19
mypy version: 1.6.1
django-stubs[compatible-mypy] version: 4.2.6
django-stubs-ext version: 4.2.5
typing-extensions version: 4.8.0
Wonderland answered 24/11, 2023 at 17:21 Comment(4)
Note that such a change breaks the Liskov Substitution Principle.Depoliti
@PawełRubin Agreed, 100% —in this specific case though, we broke the Liskov principle very intentionally, as it saves a tremendous amount of engineering, and we’re using no other User-like models.Wonderland
If your email field fulfills the contract, what about casting it to the type required in the abstract base class? (email = cast(EmailField[str | int | Combinable, str], models.EmailField(db_index=True, blank=True, null=True, unique=True)))Deathday
@Deathday But user.email needs to be allowed to be None for some objects under our custom User modelWonderland
T
1

One option is to change the super class from AbstractUser to AbstractBaseUser. This would allow to to more easily over-write the specific properties you want to change and give you more flexibility in the long run, at the cost of some extra boilerplate.

class User(AbstractBaseUser, PermissionsMixin):
   ...
   email = models.EmailField(db_index=True, blank=True, null=True, unique=True)
   ...

Some extra info: https://testdriven.io/blog/django-custom-user-model/

Thrasher answered 28/11, 2023 at 19:8 Comment(0)
C
1

One option is to overwrite the django stubs to be compatible with your user model.

Create a stub file (.pyi) for django and override the type for AbstractUser in there, see here.

Crabby answered 28/11, 2023 at 19:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.