mypy type checking issue with Django's CustomUser and BaseUserManager

Hi everyone,

I’m working on a Django project and I’m trying to use mypy to enforce type checking. To avoid circular imports, I’ve used TYPE_CHECKING and raw strings for type annotations, particularly for my CustomUser class.

When I run mypy on the entire accounts/ module, I encounter the following errors:

mypy accounts/
accounts/managers.py:16: error: "_T" has no attribute "set_password"  [attr-defined]
accounts/managers.py:18: error: Incompatible return value type (got "_T", expected "CustomUser")  [return-value]
Found 2 errors in 1 file (checked 11 source files)

However, if I run mypy on individual files (managers.py or models.py), everything works fine:

mypy managers.py
Success: no issues found in 1 source file

mypy models.py
Success: no issues found in 1 source file

Here are the relevant parts of my code:

accounts/models.py:

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models

from accounts.managers import CustomUserManager

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email: models.EmailField = models.EmailField(
        max_length=255,
        unique=True,
        blank=False
    )
    is_active: models.BooleanField = models.BooleanField(default=True)
    is_staff: models.BooleanField = models.BooleanField(default=False)
    is_superuser: models.BooleanField = models.BooleanField(default=False)
    zip_code: models.CharField = models.CharField(blank=True, max_length=5)
    objects = CustomUserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    def __str__(self) -> str:
        return self.email

    class Meta:
        verbose_name = "utilisateur"

accounts/managers.py:

from typing import Optional, TYPE_CHECKING

from django.contrib.auth.base_user import BaseUserManager

if TYPE_CHECKING:
    from accounts.models import CustomUser

class CustomUserManager(BaseUserManager):
    def create_user(self, email: str, password: Optional[str] = None) -> "CustomUser":
        if not email:
            raise ValueError("Le champ email est obligatoire")
        email = self.normalize_email(email)
        user = self.model(email=email)
        user.set_password(password)
        user.save()  # user.save(using=self._db) si on veut s'assurer que l'utilisateur est sauvegardé dans la base de données correcte
        return user

    def create_superuser(self, email: str, password: Optional[str] = None) -> "CustomUser":
        user = self.create_user(email=email, password=password)
        user.is_staff = True
        user.is_superuser = True
        user.save()  # user.save(using=self._db) si on veut s'assurer que l'utilisateur est sauvegardé dans la base de données correcte
        return user

Is this a known issue with mypy and Django, or is there a better approach to handling these type errors? Would it be advisable to ignore these specific errors, or should I refactor the code to resolve them?

Thank you ahead.

Welcome @killianpy !

Having to do those work-arounds to prevent circular import issues should be an indicator to you that your code isn’t structured optimally from Django’s perspective.

You avoid a lot of these types of issues by putting your managers with their models in your models.py file, instead of trying to split them out into separate files.

I suggest to everyone, that the default standards and practices of file organization be followed until a specific situation exists where it’s no longer practical to do so. Trying to impose a different file organization to match personal esthetics tends to create problems like this.

@KenWhitesell,

First, thank you for your welcome and for your precious advice.

I have splitted my models.py in two files because I had some import issues in the original models.py file (as you can see below). CustomUserManager is defined lower than the CustomUser class. So CustomUserManager() raise an error. If I reverse the two classes, it’s CustomUser (in CustomeUserManager’s method type hinting) that raise me the same error, could you help me to fix it properly ?

from typing import Optional

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
from django.contrib.auth.base_user import BaseUserManager


class CustomUser(AbstractBaseUser, PermissionsMixin):
    email: models.EmailField = models.EmailField(
        max_length=255,
        unique=True,
        blank=False
    )
    is_active: models.BooleanField = models.BooleanField(default=True)
    is_staff: models.BooleanField = models.BooleanField(default=False)
    is_superuser: models.BooleanField = models.BooleanField(default=False)
    zip_code: models.CharField = models.CharField(blank=True, max_length=5)
    objects = CustomUserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    def __str__(self) -> str:
        return self.email

    class Meta:
        verbose_name = "utilisateur"


class CustomUserManager(BaseUserManager):
    def create_user(self, email: str, password: Optional[str] = None) -> CustomUser:
        if not email:
            raise ValueError("Le champ email est obligatoire")
        email = self.normalize_email(email)
        user = self.model(email=email)
        user.set_password(password)
        user.save()  # user.save(using=self._db) si on veut s'assurer que l'utilisateur est sauvegardé dans la base de données correcte
        return user

    def create_superuser(self, email: str, password: Optional[str] = None) -> CustomUser:
        user = self.create_user(email=email, password=password)
        user.is_staff = True
        user.is_superuser = True
        user.save()  # user.save(using=self._db) si on veut s'assurer que l'utilisateur est sauvegardé dans la base de données correcte
        return user

You’ll generally find that Managers are defined before the Models that use them.

The docs for MyPy cover what you need to do for Forward references within a file.

“Read the doc !”

I apologize for that… I’ve modified my code with your advice, can you take a look at it ?

from __future__ import annotations

from typing import Optional, TypeVar

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
from django.contrib.auth.base_user import BaseUserManager


CustomUserType = TypeVar("CustomUserType", bound="CustomUser")


class CustomUserManager(BaseUserManager[CustomUserType]):
    """Manager class for handling the creation of custom users and superusers.

    Inherits from `BaseUserManager` and provides methods to create standard users and superusers.
    """
    def create_user(self, email: str, password: Optional[str] = None) -> CustomUserType:
        """Creates and returns a regular user with the given email and password.

        Args:
            email (str): The email address for the user. Must be provided.
            password (Optional[str], optional): The password for the user. Defaults to None for more flexibility.

        Raises:
            ValueError: If the email is not provided.

        Returns:
            CustomUser: The created user instance.
        """
        if not email:
            raise ValueError("Le champ email est obligatoire")
        email = self.normalize_email(email)
        user = self.model(email=email)
        user.set_password(password)
        user.save()  # user.save(using=self._db) if you want to ensure that the user is saved in the correct database
        return user

    def create_superuser(self, email: str, password: Optional[str] = None) -> CustomUserType:
        """Creates and returns a superuser with the given email and password.

        This method sets the `is_staff` and `is_superuser` flags to True.

        Args:
            email (str): The email address for the superuser. Must be provided.
            password (Optional[str], optional): The password for the superuser. Defaults to None.

        Returns:
            CustomUser: The created superuser instance.
        """
        user = self.create_user(email=email, password=password)
        user.is_staff = True
        user.is_superuser = True
        user.save()  # user.save(using=self._db) if you want to ensure that the user is saved in the correct database
        return user


class CustomUser(AbstractBaseUser, PermissionsMixin):
    """Custom user model that uses email as the unique identifier instead of username.

    Inherits from `AbstractBaseUser` and `PermissionsMixin` and provides custom fields like `zip_code`,
    along with the standard `is_active`, `is_staff`, and `is_superuser` flags.

    Attributes:
        email (models.EmailField): The user's email address, unique and required.
        is_active (models.BooleanField): Indicates whether the user account is active. Defaults to True.
        is_staff (models.BooleanField): Indicates whether the user can access the admin site. Defaults to False.
        is_superuser (models.BooleanField): Indicates whether the user has all permissions without explicitly assigning them. Defaults to False.
        zip_code (models.CharField): Optional field to store the user's zip code.
    """
    email: models.EmailField = models.EmailField(
        max_length=255,
        unique=True,
        blank=False
    )
    is_active: models.BooleanField = models.BooleanField(default=True)
    is_staff: models.BooleanField = models.BooleanField(default=False)
    is_superuser: models.BooleanField = models.BooleanField(default=False)
    zip_code: models.CharField = models.CharField(blank=True, max_length=5)
    objects = CustomUserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    def __str__(self) -> str:
        """Returns the string representation of the user, which is the email address.

        Returns:
            str: The user's email address.
        """
        return self.email

    class Meta:
        """Meta options for the CustomUser model."""
        verbose_name = "utilisateur"

(env) killianp@killianp ~/D/p/a/src [0|1]> mypy accounts/
Success: no issues found in 10 source files

Thank you again @KenWhitesell !