Python Dataclass Field Metadata — Core Concepts
What field metadata actually is
Every field in a Python dataclass can carry a metadata mapping — a read-only dictionary you attach via the field() function. Python’s dataclass machinery stores it but never reads it. It’s an extension point: a standardized place for libraries and your own code to stash per-field configuration.
from dataclasses import dataclass, field
@dataclass
class Product:
name: str = field(metadata={"max_length": 100, "searchable": True})
price: float = field(metadata={"currency": "USD", "min": 0.01})
sku: str = field(metadata={"db_column": "product_sku", "indexed": True})
Accessing metadata
Use dataclasses.fields() to introspect:
from dataclasses import fields
for f in fields(Product):
print(f.name, f.metadata)
# name mappingproxy({'max_length': 100, 'searchable': True})
# price mappingproxy({'currency': 'USD', 'min': 0.01})
# sku mappingproxy({'db_column': 'product_sku', 'indexed': True})
The metadata is a types.MappingProxyType — immutable after creation. You can’t accidentally modify it at runtime.
Pattern 1: validation rules
Instead of scattering validation logic across your codebase, encode rules in metadata and write a single validator:
from dataclasses import dataclass, field, fields
@dataclass
class User:
username: str = field(metadata={"min_length": 3, "max_length": 30})
age: int = field(metadata={"min": 0, "max": 150})
email: str = field(metadata={"pattern": r"^[\w.]+@[\w.]+$"})
def validate(instance):
import re
for f in fields(instance):
val = getattr(instance, f.name)
meta = f.metadata
if "min_length" in meta and len(val) < meta["min_length"]:
raise ValueError(f"{f.name} too short")
if "max_length" in meta and len(val) > meta["max_length"]:
raise ValueError(f"{f.name} too long")
if "min" in meta and val < meta["min"]:
raise ValueError(f"{f.name} below minimum")
if "max" in meta and val > meta["max"]:
raise ValueError(f"{f.name} above maximum")
if "pattern" in meta and not re.match(meta["pattern"], str(val)):
raise ValueError(f"{f.name} doesn't match pattern")
One function handles all validation by reading metadata. Adding a new rule means adding a key to metadata, not writing new validation code.
Pattern 2: serialization mapping
When your Python field names don’t match your API or database names:
@dataclass
class Config:
database_url: str = field(metadata={"env": "DATABASE_URL"})
debug_mode: bool = field(metadata={"env": "DEBUG", "cast": bool})
max_workers: int = field(metadata={"env": "MAX_WORKERS", "cast": int})
import os
def from_env(cls):
kwargs = {}
for f in fields(cls):
env_key = f.metadata.get("env", f.name.upper())
raw = os.environ.get(env_key)
if raw is not None:
cast = f.metadata.get("cast", str)
kwargs[f.name] = cast(raw)
return cls(**kwargs)
Pattern 3: documentation and schema generation
Metadata can carry human-readable descriptions that tools convert into API docs or JSON schemas:
@dataclass
class Ticket:
title: str = field(metadata={"description": "Short summary of the issue"})
priority: int = field(metadata={"description": "1 (low) to 5 (critical)", "examples": [1, 3, 5]})
def to_json_schema(cls):
props = {}
for f in fields(cls):
prop = {"type": f.type.__name__ if hasattr(f.type, '__name__') else str(f.type)}
if "description" in f.metadata:
prop["description"] = f.metadata["description"]
if "examples" in f.metadata:
prop["examples"] = f.metadata["examples"]
props[f.name] = prop
return {"type": "object", "properties": props}
Common misconception
Metadata doesn’t affect how the dataclass behaves. Setting metadata={"immutable": True} doesn’t make the field immutable — it’s just data that something else has to read and enforce. Python’s dataclass decorator completely ignores metadata contents.
When to use metadata vs other approaches
| Need | Use metadata | Use something else |
|---|---|---|
| Per-field config for your own tooling | ✅ | |
| Complex validation with dependencies | Pydantic validators | |
| Type-level constraints | Annotated[int, Gt(0)] (Pydantic/beartype) | |
| Runtime behavior changes | Descriptors or __post_init__ | |
| Library-specific config (e.g., SQLAlchemy columns) | Library’s own field system |
Metadata shines for lightweight, library-agnostic per-field annotations. For heavy validation or ORM mapping, purpose-built tools are usually better.
The one thing to remember: Field metadata is Python’s built-in extension point for per-field configuration — encode your rules there, write generic processors, and keep your data models clean.
See Also
- Python Atexit How Python's atexit module lets your program clean up after itself right before it shuts down.
- Python Bisect Sorted Lists How Python's bisect module finds things in sorted lists the way you'd find a word in a dictionary — by jumping to the middle.
- Python Contextlib How Python's contextlib module makes the 'with' statement work for anything, not just files.
- Python Copy Module Why copying data in Python isn't as simple as it sounds, and how the copy module prevents sneaky bugs.
- Python Datetime Handling Why dealing with dates and times in Python is trickier than it sounds — and how the datetime module tames the chaos