# mypy type checking issue with Django's CustomUser and BaseUserManager

**URL:** <https://forum.djangoproject.com/t/mypy-type-checking-issue-with-djangos-customuser-and-baseusermanager/34689>\
**Category:** Mystery Errors\
**Created:** [September 11, 2024, 7:25am UTC](https://forum.djangoproject.com/t/mypy-type-checking-issue-with-djangos-customuser-and-baseusermanager/34689 "2024-09-11T07:25:54Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![killianpy](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/killianpy/32/23832_2.png) [@killianpy](https://forum.djangoproject.com/u/killianpy)\
**Post date:** [September 11, 2024, 7:25am UTC](https://forum.djangoproject.com/t/mypy-type-checking-issue-with-djangos-customuser-and-baseusermanager/34689/1 "2024-09-11T07:25:54Z")

</div>

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:

```auto
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:

```auto
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`:

```auto
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`:

```auto
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.

---

<div class="post-metadata">

**Author:** ![KenWhitesell](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/kenwhitesell/32/280_2.png) [@KenWhitesell](https://forum.djangoproject.com/u/KenWhitesell)\
**Post date:** [September 11, 2024, 11:43am UTC](https://forum.djangoproject.com/t/mypy-type-checking-issue-with-djangos-customuser-and-baseusermanager/34689/2 "2024-09-11T11:43:16Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![killianpy](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/killianpy/32/23832_2.png) [@killianpy](https://forum.djangoproject.com/u/killianpy)\
**Post date:** [September 11, 2024, 1:28pm UTC](https://forum.djangoproject.com/t/mypy-type-checking-issue-with-djangos-customuser-and-baseusermanager/34689/3 "2024-09-11T13:28:30Z")

</div>

@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 ?

```auto
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

```

---

<div class="post-metadata">

**Author:** ![KenWhitesell](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/kenwhitesell/32/280_2.png) [@KenWhitesell](https://forum.djangoproject.com/u/KenWhitesell)\
**Post date:** [September 11, 2024, 10:18pm UTC](https://forum.djangoproject.com/t/mypy-type-checking-issue-with-djangos-customuser-and-baseusermanager/34689/4 "2024-09-11T22:18:19Z")

</div>

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`](https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html#forward-references) within a file.

---

<div class="post-metadata">

**Author:** ![killianpy](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/killianpy/32/23832_2.png) [@killianpy](https://forum.djangoproject.com/u/killianpy)\
**Post date:** [September 12, 2024, 8:30am UTC](https://forum.djangoproject.com/t/mypy-type-checking-issue-with-djangos-customuser-and-baseusermanager/34689/5 "2024-09-12T08:30:49Z")

</div>

“Read the doc !”

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

```auto
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"

```

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

```

Thank you again @KenWhitesell !
