#13783·pydantic

Enable support for typed `model_construct` besides mypy (incl. example implementation)

Author: maddinkunzeCreated Sep 9, 2026Updated Sep 17, 2026
Labelsfeature requesttopic-type checking

Initial Checks

  • I have searched Google & GitHub for similar requests and couldn't find anything
  • I have read and followed the docs and still think this feature is missing

Description

Hi, guys! I really love pydantic, but there is the following issue that i really want to see resolved and i hope i can help:

TL;DR: should model_construct(...) be typed (similar to __init__) and if so, what do you think about my solution of "copying" the __init__ method signature? (If not, is there any official way of instantiating a model with type-checking but without validation?)

Consider this simple model:

python
class User(BaseModel):
    id: int = Field(default_factory=lambda: randint(1, 1000))
    name: str

If one wants to create model instances with data where the types are known at type-check time, they have to choose either User(id=7, name="John Doe") or User.model_construct(id=7, name="John Doe")

The issue for me is, that the first option performs redundant (assuming type-checker correctness) validation at runtime and the second cannot perform any type-checks, because the signature is currently model_construct(**kwargs: Any).

According to your docs, the mypy plugin enables this behaviour for mypy, but a lot of users rely on other type-checking tools and it would be great if this functionality could be supported for them as well.

Here is a simple (purely types-based) solution i came up with that seems to work for this use-case and i wanted to get some feedback before making it a proper MR. Essentially it simply copies the ParamSpec of the __init__(...) function into the model_construct method:

python
from typing_extensions import Protocol, TypeVar, ParamSpec, Callable, TYPE_CHECKING
from pydantic import BaseModel

P = ParamSpec("P")
T = TypeVar("T")

class model_construct_decorator:
    def __init__(self, f): pass
    def __get__(self, instance: None, owner: Callable[P, T]) -> Callable[P, T]:
        return owner

class User(BaseModel):
    id: int = Field(default_factory=lambda: randint(1, 1000))
    name: str

    if TYPE_CHECKING:
        @model_construct_decorator
        def model_construct_typed(self): ...
    else:
        @classmethod
        def model_construct_typed(cls, *args, **kwargs):
            return cls.model_construct(*args, **kwargs)

User.model_construct_typed(id="7") # -> now also shows a type error in pyright

(The idea would be that what i currently named model_construct_typed(...) would be implemented as model_construct(...) on BaseModel)


Now there are multiple solvable issues that ive left out for brevity, but here are their solutions for anyone interested:

1. What about the _fields_set argument of the current model_construct(...)?

The model_construct_decorator can be updated as follows to reestablish that argument:

python
class ModelConstructCallable(Protocol[P, T]):
    def __call__(self, _fields_set: set[str]|None=None, *args: P.args, **kwargs: P.kwargs) -> T: ...

class model_construct_decorator:
    def __init__(self, f): pass
    def __get__(self, instance: None, owner: Callable[P, T]) -> ModelConstructCallable[P, T]:
        return lambda _fields_set=None, *args, **kwargs: owner(*args, **kwargs) # could also be a simple pass statement with type-ignore comment, because this should never actually be executed
2. Will any of the current type-checking behaviour break?

Since users currently have the **kwarg option available, this switch may break their existing code, especially if the pass extra data to model construct, as now their type-checker will complain about the extra data. There are two solutions for that issue: a) either keep the model_construct(...) as is and add a new model_construct_typed(...) b) add a new argument (e.g. _type_check: bool=False, after _fields_set) that optionally enables this behaviour

Here is an example implementation of solution b) from a type-checking perspective (at runtime there should be no difference):

python
class ModelConstructCallable(Protocol[P, T]):
    @overload
    def __call__(self, _fields_set: set[str]|None=None, _type_check: Literal[False]=False, **kwargs: Any) -> T: ...
    @overload
    def __call__(self, _fields_set: set[str]|None=None, _type_check: Literal[True], *args: P.args, **kwargs: P.kwargs) -> T: ...

class model_construct_decorator:
    def __init__(self, f): pass
    def __get__(self, instance: None, owner: Callable[P, T]) -> ModelConstructCallable[P, T]:
        return lambda _fields_set=None, _type_check=False, *args, **kwargs: owner(*args, **kwargs) # could also be a simple pass statement with type-ignore comment, because this should never actually be executed

...

User.model_construct(id="7", some_extra_value=...) # still no type error as before
User.model_construct(_type_check=True, id="7", some_extra_value="...") # type error as intended
3. What about the instance version of the current model_construct(...)?

I am not 100% sure what the behaviour is supposed to be when calling model_construct(...) on an instance of BaseModel, but i assume that it acts as a classmethod, so it should be the exact same method both on class- and instance-level. If thats the case, then the model_construct_decorator can be updated as follows to let type-checkers know that this is a valid use-case as well:

class model_construct_decorator:
    def __init__(self, f): pass
    def __get__(self, instance: Any, owner: Callable[P, T]) -> Callable[P, T]: # `instance: None` -> `instance: Any` stops type-checking errors when calling `User(...).model_construct_typed(...)`
        return owner
4. What about the current mypy plugin? and What about type-checker compatibility?

The proposed solution is solely based on typing concepts and as such, any fully-fledged type-checker should be able to infer the data correctly. That being said, mypy does not have this capability (yet?), so the mypy plugin would still be required. Beyond that, i did initial tests of the change and here is the compatibility with common type checkers:

mypy (no plugin)¹ mypy (with plugin)¹ pyright ty basedpyright pyrefly
model_construct(...)
model_construct_typed(...) ✅⚠️²

(I couldnt get the legacy pyre working and pytype is outdated and also supports neither)

¹ Note that while using model_construct_typed(...) does not enable the desired functionality in mypy without plugin, it also does not break the existing functionality in mypy with plugin.

² Note that ty has some kind of builtin support for pydantic and internally replaces id: int with id: LaxInt, so this error would be caught User.model_construct_typed(id=7) (name argument missing), but this wouldnt User.model_construct_typed(id="7", name="John Doe"), because from the perspective of ty, str is assignable to LaxInt. An update from their side would be required, but i think it is a reasonable assumption that they would add support for that change. See here for more info on the pydantic-in-ty support


Finally lets get to my main questions about all of this:

  • Is there general interest in this functionality or is it just me? If there is no interest, then how do you handle this use-case?
  • Are there any issues with my solutions that i have not addressed yet?
  • Is there anything else i should consider before starting on the actual implementation/MR?

Affected Components