</>CodeWithKarani

Pydantic mypy plugin: the false 'Unexpected keyword argument' on forbid-extra models

Karani GeoffreyKarani Geoffrey7 min read

mypy is red. It is pointing at a Pydantic model constructor and telling you, with total confidence, that you passed an argument that does not exist:

error: Unexpected keyword argument "field3" for "F"; did you mean "field1"?  [call-arg]

Except field3 does exist. It is right there in the model definition. The code runs. The tests pass. Pydantic constructs the object without complaint at runtime. And yet the type checker insists you have made a mistake, even helpfully suggesting a different field as if you fat-fingered it. You did not. This is one of those rare cases where the tool is wrong and your code is right, and the worst thing you can do is start "fixing" correct code to make a false alarm go quiet.

This is a known limitation of the pydantic mypy plugin, tied to a specific combination: a model with extra='forbid' and a type alias in the mix. It has been reported and confirmed and remains open. Knowing that up front saves you from the two bad responses everyone reaches for, which I will name and dismantle below. First, the short version.

this is a pydantic mypy plugin bug, not your bug. Do not rewrite working code.

  • The trigger is a forbid-extra BaseModel combined with a type alias referencing it; the plugin loses the real field set and flags valid keywords.
  • Suppress it at the exact call site with # type: ignore[call-arg], scoped to that line.
  • Do not disable the plugin globally, and do not rename fields to dodge it.
  • If you hit several of these, consider running Pyright, which uses dataclass_transform and does not share this exact limitation.

The exact error, and what triggers it

The message you are chasing, verbatim, is:

error: Unexpected keyword argument "field3" for "F"; did you mean "field1"?  [call-arg]

The shape that provokes it is a forbid-extra model whose construction the plugin analyses through a type alias rather than the class directly. Something structurally like this:

from pydantic import BaseModel, ConfigDict

class F(BaseModel):
    model_config = ConfigDict(extra="forbid")
    field1: int
    field2: int
    field3: int

# a type alias standing in for the model
FAlias = F

def make() -> F:
    # runs fine; field3 is a real field
    return FAlias(field1=1, field2=2, field3=3)  # plugin: Unexpected keyword argument "field3"

The precise combination that reproduces it varies with plugin and mypy versions, and the reports involve type aliases interacting with the forbid-extra configuration. The point is not to memorise one exact snippet; it is to recognise the family. When mypy claims a field that is plainly defined is "unexpected", and extra='forbid' and a type alias are both present, you are looking at the plugin limitation, not a real defect in your model.

Why the plugin gets this wrong

The pydantic mypy plugin exists because, to mypy, a Pydantic model is not a normal class. mypy cannot see that BaseModel's metaclass turns your annotated attributes into constructor parameters. So the plugin steps in and synthesises an __init__ signature for each model, one keyword parameter per field, at type-check time. That synthetic signature is what lets mypy validate your constructor calls at all.

The extra setting changes what that synthetic signature is allowed to accept. With extra='allow', arbitrary extra keywords are permitted, so the plugin makes the signature permissive. With extra='forbid', only the declared fields are legal, so the plugin makes the signature strict, one exact parameter per field and nothing else. The strict path is where the bug lives: when the model is reached through a type alias, the plugin's resolution of the real field set can fall out of sync with the strict signature it built, and a field it should recognise gets rejected as unexpected. The runtime is unaffected because Pydantic's actual metaclass, which does the real work, never went through the plugin's flawed synthesis.

Same model, two different constructors Runtime (Pydantic metaclass) Builds the real __init__ from fields field1, field2, field3 all accepted Constructs the object. No error. This is what actually executes. mypy plugin (synthesised) Builds a strict signature for forbid Type alias mis-resolves the field set field3 flagged: "Unexpected keyword" A false positive at check time only.
The runtime constructor is correct. The plugin's stand-in constructor is what is wrong, and only under this specific combination.

The fix: scope the ignore to the call site

Step 1: Confirm it really is the false positive

Before suppressing anything, prove the code is actually correct. Two checks:

# 1. The field exists on the model
print(F.model_fields.keys())
# dict_keys(['field1', 'field2', 'field3'])

# 2. Construction works at runtime
F(field1=1, field2=2, field3=3)   # no exception

If the field is in model_fields and construction runs, mypy is wrong. If construction actually raises, then it is a real error and you should fix the code, not suppress it. Do this check every time; do not assume.

Step 2: Add a scoped type: ignore with the error code

return FAlias(field1=1, field2=2, field3=3)  # type: ignore[call-arg]

The bracketed [call-arg] matters. A bare # type: ignore silences every possible error on that line, so a genuinely wrong argument you introduce later would also be hidden. Scoping to [call-arg] silences only this category, and because you run mypy with warnings on unused ignores, mypy will actively tell you if a future plugin version fixes the bug and the ignore is no longer needed. That turns a workaround into something self-cleaning.

# pyproject.toml, so stale ignores get flagged
[tool.mypy]
plugins = ["pydantic.mypy"]
warn_unused_ignores = true

Step 3: If it is widespread, weigh switching type checkers for models

One or two ignores are fine. Dozens across the codebase is a signal. Pyright understands Pydantic via the standard dataclass_transform protocol rather than a hand-written plugin, and it does not share this particular type-alias-plus-forbid limitation. It is not universally better, it has its own blind spots, but for this class of false positive it tends to be correct.

pip install pyright
pyright your_package/

You do not have to abandon mypy to benefit; some teams run mypy for general checking and Pyright for model-heavy modules, and take the intersection.

Verification

# The specific error is gone, and nothing else was suppressed
mypy your_package/
# Success: no issues found in N source files

# Prove the ignore is still load-bearing, not stale:
# temporarily remove the # type: ignore[call-arg] and re-run;
# the call-arg error should reappear. If it does not, delete the ignore.

That last step is the discipline that keeps suppressions honest. An ignore you never re-test becomes a place bugs hide. With warn_unused_ignores on, mypy does most of that policing for you.

Related plugin gaps worth recognising

This is not the only rough edge in the plugin. If you see any of these, suspect the plugin before you suspect your code:

SymptomLikely plugin limitation
Inherited field's type not inferred in a subclass constructorField type resolution across inheritance
A field literally named kwargs confuses argument checkingReserved-name collision in the synthesised signature
Valid keyword flagged as unexpected under extra='forbid'The one in this article

The common thread is the synthesised __init__. Anything that makes the real field set hard for the plugin to reconstruct, aliases, unusual names, inheritance chains, is where the synthetic signature can drift from reality.

What people get wrong

Disabling the pydantic mypy plugin project-wide. The nuclear option, and the worst one. The plugin is the only reason mypy catches genuinely wrong constructor calls, missing required fields, and bad default types on your models. Removing it to dodge one false positive blinds you to a whole category of real bugs. You lose far more than you gain.

Renaming fields to make the error disappear. Sometimes shuffling field names or order makes the plugin stop complaining. That is not a fix, it is a coincidence, and it warps your model's schema, which is a public contract if it is a FastAPI request or response body, to appease a type checker. Never change runtime behaviour to satisfy a false positive.

A bare # type: ignore. It works and it is lazy. It hides every error on the line, present and future, so the day you actually pass a wrong argument there, mypy stays silent. Always scope to [call-arg].

Assuming red means broken. Type checkers are usually right, which is exactly why it is jarring when they are not, and why people trust the tool over their own correct code. Verify against runtime, as in Step 1. If model_fields has the field and construction runs, believe the runtime.

When it is still broken

  1. The ignore does not silence it. You scoped the wrong error code. Run mypy and read the exact code in the brackets of the real message; it may be [call-arg] or something adjacent depending on version. Match it precisely.
  2. It reappears after a dependency bump. A pydantic or mypy upgrade changed the plugin's behaviour. Re-run the Step 1 verification; if the code is still correct, the ignore still belongs, and warn_unused_ignores will tell you if it does not.
  3. Runtime actually rejects the argument. Then it was never a false positive. Under extra='forbid', passing a field the model does not declare raises at construction. Check for a typo against model_fields; the checker may have been right all along.
  4. You want the models validated more strictly than the plugin manages. Run Pyright alongside mypy on the model modules. Two checkers with different implementations catch more between them than either alone, and their disagreements are exactly where the interesting cases live. Pydantic models built for strict data contracts, like the ones I use for structured LLM outputs, are worth that extra rigour.

The lesson generalises past this one bug: a type checker is a model of your program, and models have gaps. When a checker contradicts a runtime you have verified, the checker is the thing that is wrong, and the correct response is a narrow, documented, self-expiring suppression, never a rewrite of code that already works.

Frequently asked questions

Is 'Unexpected keyword argument for F' from the pydantic mypy plugin a real error?
Not when your code actually runs fine at runtime and the field exists on the model. The pydantic mypy plugin has known limitations around type aliases combined with extra='forbid', where it loses track of the real field set and reports valid keyword arguments as unexpected. It is a confirmed plugin gap, so do not rewrite correct code to satisfy it.
How do I suppress the pydantic mypy call-arg false positive?
Add a scoped # type: ignore[call-arg] on the specific constructor call the plugin misreads, not a blanket ignore and not a disabled plugin. Scoping it to the exact call site keeps every other real type check active, and the error code in brackets means mypy will warn you if that ignore ever stops being needed.
Should I disable the pydantic mypy plugin because of this?
No. Disabling the plugin project-wide throws away all the real checking it does, including catching genuinely wrong constructor arguments, missing required fields and bad default types. You would trade one narrow false positive for the loss of the whole feature. Suppress the single call site instead.
Does Pyright handle this pydantic case better than mypy?
Pyright understands pydantic through the dataclass_transform mechanism rather than a bespoke plugin, so it does not share this exact type-alias plus forbid-extra limitation. It is not strictly better across the board, it has its own gaps, but for this specific false positive many teams find Pyright reports it correctly. Running both is an option if type accuracy on models matters to you.
#Pydantic#mypy#Type Checking#Python#Pyright
Keep reading

Related articles