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
from pydantic import BaseModel, ConfigDict, ValidationError
class Lenient(BaseModel):
title: str
minutes: int
class Strict(BaseModel):
model_config = ConfigDict(extra="forbid")
title: str
minutes: int
payload = {"title": "Vectors", "minutes": 8, "minuets": 99} # typo
m = Lenient.model_validate(payload)
print("ignored :", m.model_dump(), "<- the typo vanished silently")
try:
Strict.model_validate(payload)
except ValidationError as e:
err = e.errors()[0]
print("forbid :", err["type"], "->", err["loc"], err["msg"])
Output
Or keep them
extra="allow" stores unknown keys on the model. Useful for a passthrough payload; a liability everywhere else.
Validation happens at construction. Assigning afterwards writes whatever you give it — until validate_assignment is on.
example_03.pyPydantic
from pydantic import BaseModel, ConfigDict, ValidationError
class Loose(BaseModel):
minutes: int
class Checked(BaseModel):
model_config = ConfigDict(validate_assignment=True)
minutes: int
a = Loose(minutes=8)
a.minutes = "not a number"
print("loose :", repr(a.minutes), "<- an int field holding a str")
b = Checked(minutes=8)
b.minutes = "12"
print("checked :", repr(b.minutes), "<- coerced on assignment")
try:
b.minutes = "ages"
except ValidationError as e:
print("checked :", e.errors()[0]["msg"])
Output
Frozen models are hashable
frozen=True forbids assignment entirely and lets the model be a dict key or set member.
example_04.pyPydantic
from pydantic import BaseModel, ConfigDict, ValidationError
class Point(BaseModel):
model_config = ConfigDict(frozen=True)
x: int
y: int
p = Point(x=1, y=2)
print("value:", p)
try:
p.x = 5
except ValidationError as e:
print("frozen:", e.errors()[0]["type"])
seen = {Point(x=1, y=2), Point(x=1, y=2), Point(x=3, y=4)}
print()
print("in a set :", len(seen), "unique points")
print("as a key :", {Point(x=1, y=2): "origin-ish"})
Output
String handling for form data
Three settings remove a pile of trivial validators when the input comes from humans.
example_05.pyPydantic
from pydantic import BaseModel, ConfigDict
class Raw(BaseModel):
email: str
code: str
class Tidy(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True,
str_to_lower=True)
email: str
code: str
messy = {"email": " ADA@VizLearn.IN ", "code": " ABC123 "}
print("raw :", Raw.model_validate(messy).model_dump())
print("tidy :", Tidy.model_validate(messy).model_dump())
print()
print("str_min_length works too, as a model-wide floor:")
class NoBlanks(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True, str_min_length=1)
title: str
try:
NoBlanks(title=" ")
except Exception as e:
print(" blank title:", type(e).__name__)
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
from pydantic import BaseModel, ConfigDict, ValidationError
class Row: # pretend this came from a database
def __init__(self, title, minutes):
self.title = title
self.minutes = minutes
class Plain(BaseModel):
title: str
minutes: int
class FromObj(BaseModel):
model_config = ConfigDict(from_attributes=True)
title: str
minutes: int
row = Row("Vectors", 8)
try:
Plain.model_validate(row)
except ValidationError as e:
print("without :", e.errors()[0]["type"])
print("with :", FromObj.model_validate(row))
print()
print("In Pydantic v1 this setting was called orm_mode.")
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.
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.
What does a model do by default with a key it did not declare?
The default is `extra="ignore"`. A typo in a key produces no error and no field, so the bug appears later as a value that is inexplicably a default.
Is `m.minutes = "not a number"` validated by default?
Assignment is plain Python unless `validate_assignment=True` is set. Without it, a model's declared types can quietly stop being true.
What does `frozen=True` give you besides blocking assignment?
Immutability makes the model hashable, which unlocks ordinary Python that mutable models cannot do - and `model_copy(update=...)` still produces modified versions.
What is the risk of `from_attributes=True` on an output model?
It faithfully reads whatever attribute matches a field name, and touching a lazy ORM relationship triggers queries. Output models should list only what may be seen.
Cheat sheet
model_config
Set model-wide behaviour with model_config: extra keys, frozen instances, strict mode and assignment validation in one place.
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.