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

NeedUse metadataUse something else
Per-field config for your own tooling
Complex validation with dependenciesPydantic validators
Type-level constraintsAnnotated[int, Gt(0)] (Pydantic/beartype)
Runtime behavior changesDescriptors 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.

pythonstandard-librarytypes

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