Skip to content

Use __new__() instead of __init__() in serializers - #315

Open
Viicos wants to merge 10 commits into
typeddjango:masterfrom
Viicos:fix-260
Open

Viicos wants to merge 10 commits into
typeddjango:masterfrom
Viicos:fix-260

Conversation

@Viicos

@Viicos Viicos commented Jan 8, 2023 •

Copy link
Copy Markdown
Contributor

Attempt to fix #260. This is starting to be really tricky to handle, so I don't know if this is a viable option. Regarding ModelSerializer, we might want to define the same overload, and only allow specific types for instance with respect to many (e.g. if many=False, only allow _MT, but I'm unsure there's other types that could be allowed as well).

Note: __init__ has been omitted, otherwise mypy will not infer the correct return type from __new__

@Viicos
Viicos marked this pull request as draft January 8, 2023 17:28
Comment thread rest_framework-stubs/serializers.pyi Resolved
@intgr

intgr commented Jan 9, 2023

Copy link
Copy Markdown
Contributor

This has been in the back of my mind also. It would be great if this works.

AFAIK def __new__(...) -> ListSerializer is not allowed: mypy requires __new__ return type to be a subclass of the class being defined. There's a discussion about this somewhere in mypy issues or PRs, I couldn't find it right now.

But I think it's possible to convince mypy developers to change this requirement.

Please add a test case demonstrating whether this works or not.

@intgr

intgr commented Jan 9, 2023

Copy link
Copy Markdown
Contributor

The CI errors will be fixed in #316.

@Viicos

Viicos commented Jan 9, 2023 •

Copy link
Copy Markdown
Contributor Author

AFAIK def __new__(...) -> ListSerializer is not allowed: mypy requires __new__ return type to be a subclass of the class being defined. There's a discussion about this somewhere in mypy issues or PRs, I couldn't find it right now.

Well ListSerializer is a subclass of BaseSerializer, so this should be alright I guess. Will add tests

EDIT: This is not the case for ModelSerializer, so mypy indeed complains about this:

error: Incompatible return type for "__new__" (returns "ListSerializer[_MT]", but must return a subtype of "ModelSerializer[Any]")

So I've added a type: ignore in the meanwhile

@Viicos
Viicos marked this pull request as ready for review January 9, 2023 21:43
@Viicos
Viicos marked this pull request as draft January 9, 2023 21:44
@Viicos

Viicos commented Jan 10, 2023

Copy link
Copy Markdown
Contributor Author

Ok this seems to work fine when using BaseSerializer:

IntBaseSerializer = serializers.BaseSerializer[int]
test_serializer = IntBaseSerializer([1, 2], many=True)
reveal_type(test_serializer) # N: Revealed type is "rest_framework.serializers.ListSerializer[builtins.int]"

But when defining a custom serializer:

class TestSerializer(serializers.Serializer[int]):
    pass

test_serializer = TestSerializer(instance=[1, 2], many=True)
reveal_type(test_serializer) # N: Revealed type is "main.TestSerializer"

We get the wrong type, possibly because mypy isn't using the __new__ overload defined in BaseSerializer. But I suspect this might be because ListSerializer isn't a subclass of TestSerializer, so mypy isn't giving the correct type.

I did some tests locally, and it seems that this issue is specific to mypy:

from __future__ import annotations

class A:
    def __new__(cls) -> B:
        return B()

class B(A):
    pass

class C(A):
    pass

a = C()
reveal_type(a) # Revealed type is "C", but got B with pyright (Pylance)

I'll open a ticket @mypy, and I think this could be an opportunity to discuss what you said @intgr about the requirement that the return type must be a subclass of the class being defined

overload definition is subject to change, as it is missing some
arguments.
@intgr

intgr commented Jan 12, 2023

Copy link
Copy Markdown
Contributor

I found there's already an open mypy issue about this: python/mypy#8330

@intgr

intgr commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

I think this is now unblocked via python/mypy#21441 as of mypy 2.1 or 2.2

# Conflicts:
#	rest_framework-stubs/serializers.pyi
#	tests/typecheck/test_serializers.yml
Viicos and others added 2 commits November 5, 2026 10:01
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

mypy_primer

Compared DRF stubs and plugin at d7ece7f with its target-branch parent. Differences are advisory.

Python 3.11

No diagnostic differences.

Python 3.12

No diagnostic differences.

Python 3.13

No diagnostic differences.

Python 3.14

No diagnostic differences.

Python 3.15

No diagnostic differences.

Full comparison and diagnostic artifacts

@Viicos
Viicos marked this pull request as ready for review November 5, 2026 09:08
@Viicos Viicos changed the title Use __new__ instead of __init__ in serializers Use __new__() instead of __init__() in serializers Nov 5, 2026
@intgr intgr removed blocked Blocked by some other PR, discussion or third party dependency. upstream mypy Bugs in mypy labels Nov 5, 2026
@Viicos

Viicos commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

We unfortunately still need to work around Mypy not following the spec, but the workaround is straightforward.

I'm not sure if this is safe to get, this may create regressions for users subclassing the serializers and overridding __init__(), calling super().__init__(), etc. It's been a while since I've used Django so I don't remember the potential scenarios where this could create issues.

@intgr

intgr commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

I just merged CI pyright checking to master #1112 & then merged this branch with master.

@intgr intgr self-assigned this Nov 5, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Incompatible type with many=True

2 participants