model_config

The settings that change how a whole model behaves - extra keys, mutability, assignment checking and string handling.

Overview

Where behaviour lives

Each model has settings that change how it behaves as a whole. Should it reject keys it does not know? Should it trim whitespace from every string? Should it allow its fields to be changed after creation? Those are questions about the model, not about a single field.

The answers live in model_config, a small dictionary on the class. You set it once, and it applies to every field. It is where Pydantic keeps its switches, and a handful of them come up in nearly every project.

This article explains where behaviour lives and how to read the options. It spends the most time on the extra setting, which decides what happens to keys you did not declare. It covers the fact that validation happens once, when the object is created, and what that means for later changes. It then looks at the frozen setting, which is the other answer to the problem of mutation. Along the way you will see which settings are worth knowing by heart and which you can look up when you need them. After this, the model stops being a fixed thing and becomes something you can tune.

Think of the config as the control panel for a model. You rarely need all of it, but a few switches matter a great deal. Some protect you from typos in incoming data. Some save you from cleaning strings by hand. Some make an object safe to share, because it can no longer change. The article names the ones worth remembering and shows what each does to real input. You will also see how config is inherited, so one base class can set the rules for a whole family of models. Learning the handful that matter will save you from writing the same defensive code again and again. Reach for this article whenever a model behaves in a way you did not expect.

all so far has been per field. model_config is per model. A ConfigDict assigned in the class body that changes how the whole thing behaves.

class Module(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    title: str

In Pydantic v1 this was an inner class Config. That still works and is deprecated. It's the clearest signal that a tutorial predates v2.

There are a lot of settings. These are the ones that come up constantly.

Worth knowing

extra takes "ignore" (the default), "forbid" or "allow". Forbid catches typos in keys; the default silently drops them.
Validation runs at construction only. validate_assignment=True extends it to later assignments.
frozen=True blocks assignment and makes the model hashable, so it can be a dict key or live in a set.
str_strip_whitespace, str_to_lower and str_min_length apply to every string field and remove a lot of trivial validators.
from_attributes=True lets model_validate read attributes off any object — the setting called orm_mode in v1.
Config is inherited, so a base model with your house settings gives every subclass the same behaviour.

model_config: Settings That Change the Whole Model

Extra keys, mutability, set checking and string handling - the switches worth knowing.

Unknown keys are ignored by default

Extra keys in the input are silently dropped. That is forgiving and it hides typos, which is why extra="forbid" exists.

example_01.pyPydantic
Output

Or keep them

extra="allow" stores unknown keys on the model. Useful for a passthrough payload; a liability everywhere else.

example_02.pyPydantic
Output

Assignment is not checked unless you ask

Validation happens at construction. Assigning afterwards writes whatever you give it — until validate_assignment is on.

example_03.pyPydantic
Output

Frozen models are hashable

frozen=True forbids assignment entirely and lets the model be a dict key or set member.

example_04.pyPydantic
Output

String handling for form data

Three settings remove a pile of trivial validators when the input comes from humans.

example_05.pyPydantic
Output

Reading from objects, not just dicts

from_attributes lets a model validate anything with matching attributes — which is how a model reads an ORM row.

example_06.pyPydantic
Output

extra: what to do with keys you did not declare

The default is "skip" — unknown keys are silently dropped.

That is forgiving, and it hides mistakes. A caller sending minuets instead of minutes gets no error and no field. The model is built with what default minutes has, and the bug surfaces later as a value that is inexplicably wrong.

extra="forbid" rejects them:

model_config = ConfigDict(extra="forbid")

Now the typo is an error naming the offending key. For an inner API, a config file, or anything where you control both ends, this is almost always the better default. It converts a silent misunderstanding into an direct one.

The arg for "skip" is forward match on a public API. A client sending fields from a newer version shouldn't break against an older server. That is a real consideration. It applies to *your* API's request models rather than to each model in your code.

extra="allow" keeps unknown keys, storing them on the model and exposing them through model_extra. It's right for a genuine passthrough. A webhook body you forward, an envelope whose payload you don't own. all over else it turns your model into a dict with extra steps. Loses the promise that the fields on the object are the fields you declared.

Validation happens once

This surprises people, and it's worth being clear about.

A model is checked when it's made. Assigning to an field later is a plain Python set: no checking, no coercion.

m = Module(minutes=8)
m.minutes = "not a number"      # allowed, and now the field is a string

The thinking is speed — most models are built, read and discarded. Checking each set would be work for a case that rarely arises.

validate_assignment=True turns it on. sets are then checked and coerced like constructor args. m.minutes = "12" gives you 12 and m.minutes = "ages" raises.

Turn it on for any model that is truly mutated after build, above all one holding config or building up state. It costs a little per set and drops a class of bug where a model's declared types quietly stop being true.

Note that it also makes any model_validator(mode="after") run again on each set. This Is often what you want and is worth knowing if those checkers are costly.

frozen: the other answer to mutation

frozen=True forbids set altogether. An attempt raises a ValidationError with type frozen_instance.

It also makes the model hashable. It can be a dict key or a set member. Which unlocks a lot of plain Python that mutable models can't do.

For checked data that arrives from outside and is then only read, frozen is the right default and is under-used. It drops the question "did anything change this?" fully, it makes the object safe to share across threads or pass anywhere without defensive copying. It makes the intent clear.

model_copy(update={...}) still works on a frozen model. Making a modified version is one line. That is the functional-update pattern. It's often clearer than mutation anyway.

Single fields can be frozen with Field(frozen=True) when only part of the model should be fixed. An id that must never change while the rest of the record can.

String settings for human input

Three settings that apply to each string field:

str_strip_whitespace=True strips leading and trailing whitespace. For form data this is nearly always correct. It drops a pile of one-line checkers.

str_to_lower=True and str_to_upper=True norm case. handy for emails, codes and identifiers. Wrong for anything with display text. Apply it to models that are all identifiers rather than reaching for it globally.

str_min_length=1 sets a floor for each string. Mixed with stripping, this is the compact way to say "no blank strings anywhere in this model". This Is a rule most form models want and few state.

from_attributes: reading objects

By default model_validate expects a mapping. from_attributes=True lets it read fields off any object:

class ModuleOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    title: str
    minutes: int

ModuleOut.model_validate(orm_row)

This is how a model turns a DB row into a response. It recurses. A row with related objects turns nested models without any manual conversion.

It was orm_mode in v1. This Is the name most existing material uses.

Two cautions. It reads fields. So A lazy-loading ORM link will be *loaded* when the model touches it. Which is how a single output quietly turns fifty queries. And it will read any field matching a field name, so a model with a field called password_hash pointed at a user row will faithfully put the hash in your response. Output models should list only what may be seen.

A few more worth knowing

populate_by_name=True lets a field be filled by either its alias or its Python name. It pairs with aliases and gets a module of its own next tier.

use_enum_values=True stores the value rather than the enum member. It makes dumps simpler and loses the member's action. Often the str, Enum mixin is the better answer.

validate_default=True checks default values, which are else trusted.

arbitrary_types_allowed=True permits fields of types Pydantic knows nothing about, checked only by isinstance. It's an escape hatch for wrapping a third-party object. A sign you might want a custom type instead.

Config is inherited

A base model's config applies to each subclass, and a subclass can replace single settings.

That makes a house base model a truly good pattern:

class Base(BaseModel):
    model_config = ConfigDict(extra="forbid",
                              str_strip_whitespace=True,
                              validate_assignment=True)

each model in the code takes from from it and gets the same defaults, decided once. It's much better than the same ConfigDict copied into forty classes, four of which will end up other for no reason anybody remembers.

Suggested defaults

For request models on a public API: extra="skip" for forward match, str_strip_whitespace=True.

For inner and config models: extra="forbid". A typo is an error rather than a mystery.

For checked data that is then only read: frozen=True.

For anything mutated after build: validate_assignment=True.

Settings that catch mistakes

Three more worth knowing because each closes a exact hole.

validate_default=True checks default values. This Are else trusted. A mistyped default sits silently until a thing downstream does math on a string. Worth turning on for models whose defaults come from constants defined elsewhere.

revalidate_instances="always" re-validates a model instance passed into another model. By default an object that is already the right class is accepted as-is, on the fair assumption it was checked when built. If it was mutated later without validate_assignment, that assumption is wrong. This setting closes the gap at the cost of re-running check.

ser_json_timedelta and ser_json_bytes control how those two types serialise. This Matters when a consumer expects seconds rather than an ISO duration.

Protected namespaces

By default Pydantic reserves the model_ prefix and warns if you declare a field starting with it. That is where its own methods live — model_dump, model_validate, model_fields.

That is a genuine problem if your domain has fields like model_name or model_version. This Is common in anything ML-adjacent.

protected_namespaces=() turns the warning off, and protected_namespaces=("model_config",) narrows it. Be aware that a field truly named model_dump would shadow the method. The warning is doing real work. Disable it on purpose rather than to silence noise.

Where to set config

Three places, in growing order of scope.

On the class, as model_config = ConfigDict(...). clear and local.

On a shared base model, from parent by all. Best for house rules.

As a keyword in the class meaning. class Module(BaseModel, frozen=True). Which is compact and less discoverable.

For a code of any size, the base model is the answer. One file states the rules, each model follows them. Changing a rule is one edit rather than forty.

Config and inheritance

Config merges rather than replaces. A subclass setting one key keeps its parent's other settings. This Is what makes the base-model pattern hands-on.

That also means an from parent setting can be surprising in a subclass that didn't ask for it. If a base sets extra="forbid" and a subclass truly needs passthrough, it must say so by name. Which is the right default. Silently permissive is worse than by name permissive.

A worked house base

class Base(BaseModel):
    model_config = ConfigDict(
        extra="forbid",
        str_strip_whitespace=True,
        validate_assignment=True,
        populate_by_name=True,
        alias_generator=to_camel,
    )

Five choices, made once. Typos in keys are errors. Whitespace never reaches a field. sets are checked. Fields accept either spelling. The whole API speaks camelCase.

each model taking from from that is steady with every other. A new developer takes from the rules without being told them.

The one thing to avoid is a base so opinionated that half the models replace half of it. If a setting is wrong for a third of your models, it doesn't belong in the shared base.

model_config: summary

model_config is where model-wide action is decided. The four that matter most: extra for unknown keys, validate_assignment for mutation, frozen for immutability and hashability. The str_* family for human input.

Set them on purpose rather than by default, put your house settings on a shared base. Note that the two most common silent bugs in this area. An skipped typo in a key and an unvalidated set. Are both one config entry away from being loud.

model_config: the short version

Four settings account for most of the value.

extra="forbid" turns a typo in a key from a silent nothing into a named error. Set it anywhere you control both ends.

validate_assignment=True extends check past build. So A model's declared types stay true for its whole life.

frozen=True makes checked data immutable and hashable. This Is the right shape for anything that arrives from outside and is then only read.

str_strip_whitespace=True drops a group of trivial checker and a group of trivial bug.

Put them on a shared base, decide them once. Let each model in the code inherit the same rules.

Mistakes people make with model_config

**Leaving extra at its default where you control both ends.** A typo in a key makes no error, no field and a value that silently falls back to a default. extra="forbid" turns a mystery into a message.

given set is checked. It isn't. A model built right can hold anything at all a moment later. The declared types quietly stop being true.

**Copying the same ConfigDict into forty classes.** They drift. Four of them end up other for no reason anybody remembers. A shared base makes the rule one edit.

**Using from_attributes on an output model without listing fields carefully.** It reads what field matches a field name. A model pointed at a user row will faithfully serialise a password hash. It also triggers lazy ORM links. This Is how one output turns fifty queries.

Silencing the protected-namespace warning reflexively. A field truly named model_dump would shadow the method. Disable the warning on purpose for a domain that needs model_ names, not to quieten noise.

Building a base so opinionated that half the models replace it. If a setting is wrong for a third of your models, it isn't a house rule and doesn't belong in the shared base.

Questions people ask about model_config

How do I set model configuration in v2? Assign model_config = ConfigDict(...) in the class body. The old inner class Config still works but is deprecated.

How do I stop a model accepting unknown keys? Set extra="forbid". The default is "ignore", which drops unknown keys silently, and "allow" keeps them.

How do I make a model immutable? Use frozen=True. Assigning to a field then raises an error, and the model also becomes hashable.

What does from_attributes=True do? It lets model_validate read attributes from an arbitrary object, such as an ORM row, instead of requiring a dict.

Is config inherited by subclasses? Yes, and a subclass's model_config is merged with its parent's. Keys you set override the parent, and the rest carry over.

What does validate_assignment do? It re-runs validation whenever you set an attribute after creation. Without it, m.age = "oops" is accepted silently.

What it is really for

Config is where a code records the choices it has made about its own data.

Whether an surprise key is a mistake or a courtesy. Whether checked objects may change. Whether the wire speaks camelCase. Whether a blank string is a value.

Those are real choices, and each code makes them. Often silently, differently in other files, and rediscovered by each new person. Writing them once, on a shared base, turns a set of accidents into a rule.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What does a model do by default with a key it did not declare?

  2. Is `m.minutes = "not a number"` validated by default?

  3. What does `frozen=True` give you besides blocking assignment?

  4. What is the risk of `from_attributes=True` on an output model?

Cheat sheet

model_config

Set model-wide behaviour with model_config: extra keys, frozen instances, strict mode and assignment validation in one place.

PYDANTIC · vizlearn.in/pydantic/model_config.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.