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
You could try and use type annotations that are compatible with the
AbstractUserbase class, but also allowNonevalues for theemailfield in your customUsermodel.If you explicitly type annotate the
emailfield asOptional[models.EmailField], that would indicate thatemailcan be of typemodels.EmailFieldorNone, which should satisfy the requirements of both Django and mypy.By using the
Optionaltype annotation, you would maintain the integrity of your customUsermodel, but also adhering to mypy's type checking standards. No more "incompatible types" error.It means the type definition for your
emailfield in the customUsermodel is not fully compatible with the type expected by theAbstractUserclass. Given that mypy is strict about type consistency, especially when overriding fields in a subclass, you will need to make sure your field definition matches the expected type inAbstractUserwhile also allowingNone.Create a type variable that encompasses the types allowed by
AbstractUserand your additional requirement ofNone.And use this type variable in the field definition to satisfy both the base class's and your custom requirements.
EmailFieldTypeis now a type variable that allows theemailfield to be either amodels.EmailFieldorNone, aligning with the types specified inAbstractUser. The use ofUnionin the field definition should accommodate the specific type requirements fromAbstractUseras well as your custom model's need to allowNone.Instead of using a
TypeVar, you could directly useUnionto specify that theemailfield can be either of the types required byAbstractUserorNone. That would simplify the type definition and should be more straightforward for mypy to interpret.emailfield is declared as a union ofmodels.EmailFieldandNone. That should satisfy the type requirement forAbstractUserwhile also allowingNoneas a value.Then you would need a more sophisticated type handling or a workaround that aligns with mypy's expectations, without compromising the integrity of your Django model.
Try and create a custom subclass of
models.EmailFieldthat explicitly handles the nullable scenario in a way that is compatible with both Django and mypy.As a more drastic measure, you can try completely redefining the field and handling the migration carefully. That approach would entail removing the
emailfield from your model and then re-adding it with the new specifications. That is more intrusive and requires careful migration handling.