Chapter 3: Configuring Your Blueprint - Model Settings
In Chapter 1, we learned about BaseModel
as our data blueprint, and in Chapter 2, we saw how Field()
lets us add specific notes (like defaults or aliases) to individual rooms (fields) on that blueprint.
But what about instructions that apply to the entire blueprint? Imagine needing rules like:
- “Absolutely no extra furniture allowed that’s not in the plan!” (Forbid extra fields)
- “Once built, nothing inside can be changed!” (Make the model immutable/frozen)
- “All room names on the final report should be lowercase.” (Apply a naming convention during output)
These are model-wide settings, not specific to just one field. Pydantic provides a way to configure this overall behavior using model configuration.
Why Configure the Whole Model?
Let’s consider a simple Product
model:
from pydantic import BaseModel
class Product(BaseModel):
item_id: int
name: str
price: float | None = None
This works, but we might want to enforce stricter rules or change default behaviors:
- Strictness: What if we receive data like
{'item_id': 123, 'name': 'Thingy', 'color': 'blue'}
? By default, Pydantic ignores the extracolor
field. We might want to reject data with unexpected fields. - Immutability: What if, once a
Product
object is created, we want to prevent accidental changes likeproduct.price = 99.99
? - Naming Conventions: What if our API expects JSON keys in
camelCase
(likeitemId
) instead of Python’s standardsnake_case
(item_id
)?
These global behaviors are controlled via Pydantic’s configuration system.
Introducing ConfigDict
and model_config
Pydantic allows you to customize model behavior by adding a special class attribute called model_config
. This attribute should be assigned a dictionary-like object called ConfigDict
.
Think of model_config = ConfigDict(...)
as the master instruction sheet or the global settings panel attached to your BaseModel
blueprint. It provides overarching rules for how Pydantic should handle the model.
ConfigDict
: A special dictionary (specifically, a TypedDict
) provided by Pydantic where you specify configuration options using key-value pairs. model_config
: The class attribute on your BaseModel
where you assign your ConfigDict
.
Let’s add some configuration to our Product
model:
# Import ConfigDict
from pydantic import BaseModel, ConfigDict
class Product(BaseModel):
# Define model-wide settings here
model_config = ConfigDict(
frozen=True, # Setting 1: Make instances immutable
extra='forbid', # Setting 2: Forbid extra fields during input validation
validate_assignment=True # Setting 3: Re-validate fields when they are assigned a new value
)
item_id: int
name: str
price: float | None = None
# --- How these settings affect behavior ---
# 1. Forbid Extra Fields ('extra=forbid')
try:
# Input data has an extra 'color' field
product_data_extra = {'item_id': 123, 'name': 'Thingy', 'color': 'blue'}
Product(**product_data_extra)
except Exception as e:
print(f"Error on extra field:\n{e}")
# Expected Output (simplified):
# Error on extra field:
# 1 validation error for Product
# color
# Extra inputs are not permitted [type=extra_forbidden, ...]
# 2. Immutability ('frozen=True')
product = Product(item_id=456, name="Gadget")
print(f"Initial product: {product}")
# Expected Output: Initial product: item_id=456 name='Gadget' price=None
try:
# Attempt to change a field on the frozen instance
product.name = "New Gadget"
except Exception as e:
print(f"\nError on assignment to frozen model:\n{e}")
# Expected Output (simplified):
# Error on assignment to frozen model:
# 1 validation error for Product
# name
# Instance is frozen [type=frozen_instance, ...]
# 3. Validate Assignment ('validate_assignment=True')
product_mutable = Product.model_construct(item_id=789, name="Widget") # Use model_construct to bypass initial __init__ validation for demo
try:
# Attempt to assign an invalid type (int instead of str)
product_mutable.name = 999
except Exception as e:
print(f"\nError on invalid assignment:\n{e}")
# Expected Output (simplified):
# Error on invalid assignment:
# 1 validation error for Product
# name
# Input should be a valid string [type=string_type, input_value=999, input_type=int]
By adding the model_config
dictionary, we changed the fundamental behavior of our Product
model without altering the field definitions themselves.
Common Configuration Options
Here are a few more useful options you can set in ConfigDict
:
alias_generator
: Automatically generate aliases for fields. Often used to convert betweensnake_case
andcamelCase
.from pydantic import BaseModel, ConfigDict from pydantic.alias_generators import to_camel # Import a helper class User(BaseModel): user_id: int first_name: str model_config = ConfigDict( alias_generator=to_camel, # Use the camelCase generator populate_by_name=True # Allow using EITHER alias or python name for input (see warning below) # Replaced by validate_by_name=True + validate_by_alias=True ) # Input using camelCase aliases user_data_camel = {'userId': 1, 'firstName': 'Arthur'} user = User(**user_data_camel) print(f"User created from camelCase: {user}") # Expected Output: User created from camelCase: user_id=1 first_name='Arthur' # Output (dumping) using aliases requires `by_alias=True` print(f"Dumped with aliases: {user.model_dump(by_alias=True)}") # Expected Output: Dumped with aliases: {'userId': 1, 'firstName': 'Arthur'} print(f"Dumped without aliases: {user.model_dump()}") # Expected Output: Dumped without aliases: {'user_id': 1, 'first_name': 'Arthur'}
- Modern Alias Control (Pydantic >= v2.11): Instead of
populate_by_name
, usevalidate_by_alias
,validate_by_name
, andserialize_by_alias
for finer control:from pydantic import BaseModel, ConfigDict from pydantic.alias_generators import to_camel class UserV2(BaseModel): user_id: int first_name: str model_config = ConfigDict( alias_generator=to_camel, validate_by_name=True, # Allow input using 'user_id', 'first_name' validate_by_alias=True, # Allow input using 'userId', 'firstName' (default is True) serialize_by_alias=True # Use aliases ('userId', 'firstName') when dumping by default ) user_data_camel = {'userId': 1, 'firstName': 'Zaphod'} user_camel = UserV2(**user_data_camel) print(f"User from camel: {user_camel}") # > User from camel: user_id=1 first_name='Zaphod' user_data_snake = {'user_id': 2, 'first_name': 'Ford'} user_snake = UserV2(**user_data_snake) print(f"User from snake: {user_snake}") # > User from snake: user_id=2 first_name='Ford' # serialize_by_alias=True means model_dump() uses aliases by default print(f"Dumped (default alias): {user_camel.model_dump()}") # > Dumped (default alias): {'userId': 1, 'firstName': 'Zaphod'} print(f"Dumped (force no alias): {user_camel.model_dump(by_alias=False)}") # > Dumped (force no alias): {'user_id': 1, 'first_name': 'Zaphod'}
- Modern Alias Control (Pydantic >= v2.11): Instead of
use_enum_values
: When serializing (e.g., withmodel_dump
), use the value of an enum member instead of the member itself.from enum import Enum from pydantic import BaseModel, ConfigDict class Status(Enum): PENDING = "pending" PROCESSING = "processing" COMPLETE = "complete" class Order(BaseModel): order_id: int status: Status model_config = ConfigDict( use_enum_values=True # Use the string value of Status ) order = Order(order_id=101, status=Status.PROCESSING) print(f"Order object status type: {type(order.status)}") # Expected Output: Order object status type: <enum 'Status'> print(f"Order dumped: {order.model_dump()}") # Expected Output: Order dumped: {'order_id': 101, 'status': 'processing'} # Note: 'status' is the string "processing", not Status.PROCESSING
str_strip_whitespace
/str_to_lower
/str_to_upper
: Automatically clean string inputs.from pydantic import BaseModel, ConfigDict class Comment(BaseModel): text: str author: str model_config = ConfigDict( str_strip_whitespace=True, # Remove leading/trailing whitespace str_to_lower=True # Convert to lowercase ) comment_data = {'text': ' Hello World! ', 'author': ' ALICE '} comment = Comment(**comment_data) print(comment) # Expected Output: text='hello world!' author='alice'
You can find the full list of configuration options in the Pydantic documentation for ConfigDict
.
Important Note: Configuration set in model_config
generally applies during validation and serialization. For example, alias_generator
helps Pydantic understand incoming data with aliases and optionally use aliases when producing output, but the internal attribute name in your Python code remains the Python name (e.g., user_id
).
What About ConfigWrapper
? (Internal Detail)
You might see ConfigWrapper
mentioned in Pydantic’s internal code or documentation.
Analogy: If ConfigDict
is the settings form you fill out (frozen=True
, extra='forbid'
), then ConfigWrapper
is the internal manager object that Pydantic creates from your form. This manager holds onto your settings, knows the default values for settings you didn’t specify, and provides a consistent way for the rest of Pydantic (like the schema builder) to ask “Is this model frozen?” or “What should happen with extra fields?”.
Key Point: As a user writing Pydantic models, you almost always interact with ConfigDict
via the model_config
attribute. You generally don’t need to create or use ConfigWrapper
directly. It’s an internal helper that makes Pydantic’s life easier.
Under the Hood: How Configuration is Applied
Let’s refine our understanding of how a BaseModel
class gets created, now including configuration.
High-Level Steps:
When Python creates your Product
class:
- Inspection: Pydantic’s
ModelMetaclass
inspects the class definition. It finds the fields (item_id: int
, etc.) and also looks for themodel_config
attribute. - Config Processing: If
model_config
(aConfigDict
) is found, Pydantic uses it (along with config from any parent classes) to create an internalConfigWrapper
instance. This wrapper standardizes access to all config settings, applying defaults for any missing options. - FieldInfo Creation: It processes field definitions, potentially using
Field()
as discussed in Chapter 2, creatingFieldInfo
objects. - Schema Generation: Pydantic now uses both the
FieldInfo
objects and the settings from theConfigWrapper
to generate the detailed internal Core Schema. For example, if theConfigWrapper
saysfrozen=True
, this instruction is baked into the Core Schema. - Validator/Serializer Creation: Optimized validator and serializer functions are created based on this final Core Schema.
Sequence Diagram:
This diagram shows how model_config
influences the process:
sequenceDiagram
participant Dev as Developer
participant Py as Python
participant Meta as ModelMetaclass
participant CfgWrap as ConfigWrapper
participant Core as Pydantic Core Engine
Dev->>Py: Define `class Product(BaseModel): model_config = ConfigDict(frozen=True, extra='forbid') ...`
Py->>Meta: Ask to create `Product` class
Meta->>Meta: Find `model_config` dict in namespace
Meta->>CfgWrap: Create `ConfigWrapper` using `model_config` (and defaults)
CfgWrap-->>Meta: Return `ConfigWrapper(config_dict={'frozen': True, 'extra': 'forbid', ...other defaults...})`
Meta->>Meta: Collect fields (`item_id`, `name`, `price`) and their FieldInfo
Meta->>Core: Request Core Schema using FieldInfo AND ConfigWrapper settings (e.g., frozen, extra)
Core-->>Meta: Provide Core Schema incorporating model-wide rules
Meta->>Core: Request validator/serializer from Core Schema
Core-->>Meta: Provide optimized validator/serializer reflecting config
Meta-->>Py: Return fully prepared `Product` class
Py-->>Dev: `Product` class is ready, respecting the config
The ConfigWrapper
acts as a bridge, translating the user-friendly ConfigDict
into instructions the Core Engine understands when building the schema and validators.
Code Location:
ConfigDict
: Defined inpydantic/config.py
. It’s essentially aTypedDict
listing all valid configuration keys.ConfigWrapper
: Defined inpydantic._internal._config.py
. Its__init__
takes the config dictionary. TheConfigWrapper.for_model
class method is used by the metaclass to gather configuration from base classes and the current class definition. Itscore_config
method translates the stored config into the format needed bypydantic-core
.ModelMetaclass
: Inpydantic._internal._model_construction.py
, the__new__
method callsConfigWrapper.for_model
and passes the resulting wrapper tobuild_schema_generator
and ultimatelycomplete_model_class
, which coordinates schema and validator/serializer creation.
# Simplified view from pydantic/config.py
# ConfigDict is a TypedDict listing allowed keys and their types
class ConfigDict(TypedDict, total=False):
frozen: bool
extra: Literal['allow', 'ignore', 'forbid'] | None
alias_generator: Callable[[str], str] | None
# ... many more options
# Simplified view from pydantic._internal._config.py
class ConfigWrapper:
config_dict: ConfigDict # Stores the actual config values
def __init__(self, config: ConfigDict | dict[str, Any] | type[Any] | None, *, check: bool = True):
# Simplification: Stores the input config, potentially validating keys
self.config_dict = prepare_config(config) # prepare_config handles defaults/deprecation
# Provides attribute access like wrapper.frozen, falling back to defaults
def __getattr__(self, name: str) -> Any:
try:
return self.config_dict[name]
except KeyError:
# Fallback to default values defined in config_defaults
# return config_defaults[name] # Simplified
pass # Actual implementation is more complex
# Used during model creation to gather config from all sources
@classmethod
def for_model(cls, bases: tuple[type[Any], ...], namespace: dict[str, Any], kwargs: dict[str, Any]) -> Self:
config_new = ConfigDict()
# 1. Inherit config from base classes
# 2. Get config from 'model_config' in the current class namespace
# 3. Get config from kwargs passed during class definition (e.g., class Model(BaseModel, frozen=True): ...)
# ... logic to merge these sources ...
return cls(config_new) # Return a wrapper with the final merged config
# Creates the config dictionary specifically for pydantic-core
def core_config(self, title: str | None) -> core_schema.CoreConfig:
# Extracts relevant keys from self.config_dict and maps them
# to the names expected by pydantic_core.CoreConfig
# e.g., {'extra': 'forbid'} becomes {'extra_fields_behavior': 'forbid'}
core_options = { ... }
return core_schema.CoreConfig(**core_options)
# Simplified view from pydantic._internal._model_construction.py (ModelMetaclass.__new__)
def __new__(mcs, name, bases, namespace, **kwargs):
# ... lots of setup ...
# Step 1: Gather configuration
config_wrapper = ConfigWrapper.for_model(bases, namespace, kwargs) # Merges config from bases, class def, kwargs
# Step 2: Prepare schema generator using the config
schema_generator = build_schema_generator(
cls, # The class being built
config_wrapper,
# ... other args ...
)
# Step 3: Build core schema, validator, serializer (using schema_generator which uses config_wrapper)
# core_schema = schema_generator.generate_schema(cls) # Simplified
# validator = SchemaValidator(core_schema, config_wrapper.core_config()) # Simplified
# serializer = SchemaSerializer(core_schema, config_wrapper.core_config()) # Simplified
# ... attach schema, validator, serializer to the class ...
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
# cls.__pydantic_validator__ = validator
# ...
return cls
This setup ensures that the model-wide rules defined in model_config
are consistently applied during both validation (creating model instances) and serialization (dumping model instances).
Conclusion
You’ve learned how to configure the overall behavior of your BaseModel
blueprints:
- Use the
model_config
class attribute, assigning it aConfigDict
. ConfigDict
acts as the master instruction sheet or settings panel for the model.- Common settings include
frozen
,extra
,alias_generator
,use_enum_values
, and string cleaning options. - Pydantic uses this configuration, often via the internal
ConfigWrapper
, to tailor the validation and serialization logic defined in the Core Schema.
With BaseModel
, Field
, and ConfigDict
, you have powerful tools to define the structure, field-specific details, and overall behavior of your data models.
But what if you need logic that goes beyond simple configuration? What if you need custom validation rules that depend on multiple fields, or complex transformations before or after validation/serialization? That’s where Pydantic’s decorators come in.
Next: Chapter 4: Custom Logic (Decorators & Annotated Helpers)
Generated by AI Codebase Knowledge Builder