Skip to main content

Dynamically extract and subset Pydantic V2 models using dot-notation while preserving validators.

Project description

pydantic-pick logo

Tests Python Version Pydantic v2 License

pydantic-pick

Dynamically create subsets of Pydantic V2 models using pick_model or omit_model. Preserves validators, methods, and constraints.

This library provides two approaches for dynamically creating model subsets:

  • pick_model - Keep only the fields you specify. Everything else is dropped.
  • omit_model - Remove only the fields you specify. Everything else is kept.

Both functions preserve your Field constraints, @field_validator logic, @computed_field properties, custom methods, ClassVar attributes, and model_config settings. They handle nested models and standard library types: List, Dict, Tuple, Set, Union, Optional, and Annotated.

Installation

pip install pydantic-pick

Note: This library requires pydantic >= 2.0.0 and Python 3.10+. It is deeply tied to Pydantic V2's core architecture and is not compatible with Pydantic V1.

Quick Start

Both functions take a base model, a tuple of dot-notation paths, and a name for the new class.

Using pick_model

Specify which fields to keep. Everything else is dropped.

from pydantic import BaseModel, Field, field_validator
from pydantic_pick import pick_model

class DBUser(BaseModel):
    id: int = Field(..., ge=1)
    username: str
    password_hash: str
    email: str
    is_active: bool = True

    @field_validator("username")
    @classmethod
    def check_username(cls, v: str):
        if "admin" in v.lower():
            raise ValueError("Reserved username")
        return v

# Keep only 'id' and 'username', drop everything else
PublicUser = pick_model(DBUser, ("id", "username"), "PublicUser")

user = PublicUser(id=10, username="alice")
print(user.model_dump())
# {'id': 10, 'username': 'alice'}

# Validators and constraints still work
PublicUser(id=-5, username="bob")      # Fails: id must be >= 1
PublicUser(id=1, username="admin123")  # Fails: Reserved username

Using omit_model

Specify which fields to remove. Everything else stays.

from pydantic_pick import omit_model

# Remove 'password_hash' and 'email', keep everything else
PublicUser = omit_model(DBUser, ("password_hash", "email"), "PublicUser")

user = PublicUser(id=10, username="alice", is_active=True)
print(user.model_dump())
# {'id': 10, 'username': 'alice', 'is_active': True}

# Same validator and constraint behavior
PublicUser(id=-5, username="bob")      # Fails: id must be >= 1
PublicUser(id=1, username="admin123")  # Fails: Reserved username

Both approaches preserve your Field constraints, validators, and methods.

Deep Nesting & Complex Types

Both functions handle nested models and standard library generics: List, Dict, Tuple, Set, Union, Optional, Annotated.

class Profile(BaseModel):
    avatar_url: str
    billing_secret: str

class Account(BaseModel):
    user_id: int
    profiles: list[Profile]

With pick_model - specify what to keep:

paths = ("user_id", "profiles.avatar_url")
PublicAccount = pick_model(Account, paths, "PublicAccount")
# Keeps user_id and avatar_url. Drops everything else including billing_secret.

With omit_model - specify what to remove:

paths = ("profiles.billing_secret",)
PublicAccount = omit_model(Account, paths, "PublicAccount")
# Keeps everything except billing_secret.

Advanced Use Case: LLM Context Compression

When building autonomous AI agents, tool responses (like executing a Python script or scraping a webpage) can return thousands of lines of raw output. You can use either function to compress this before sending to the LLM.

With pick_model - keep only what you need:

from pydantic import BaseModel
from pydantic_pick import pick_model

class ToolResponse(BaseModel):
    tool_response: str  # Heavy output
    tool_close_instructions: str = "Analyze the tool_response above."
    execution_time: float
    exit_code: int

CompressedResponse = pick_model(
    ToolResponse, 
    ("tool_close_instructions", "execution_time"),  # Keep only these
    "CompressedResponse"
)

With omit_model - remove what you don't need:

from pydantic_pick import omit_model

CompressedResponse = omit_model(
    ToolResponse, 
    ("tool_response",),  # Only drop the heavy field
    "CompressedResponse"
)

Performance Tip: Both functions use functools.lru_cache. Generating a model dynamically takes a few milliseconds, but subsequent calls requesting the exact same subset of the same model return instantly from memory. It is completely safe to use inside fast-paced API endpoints or intensive AI agent loops.

What Survives Extraction?

Unlike naive create_model wrappers, this library actively preserves your business logic:

  • Field Constraints: Everything inside Field(...) (like ge, max_length, alias).
  • Field Validators: @field_validator logic is preserved (as long as the fields it targets were not omitted).
  • Computed Fields: @computed_field properties are safely carried over.
  • Methods: Custom instance methods, @classmethod, @staticmethod, and custom wrappers.
  • ClassVars: typing.ClassVar attributes are safely mapped.
  • Config: Your model_config (like frozen=True or alias_generator) is inherited.

Intelligent Dependency Resolution (AST Parsing)

What happens if you have a @computed_field or a custom method that relies on a data field, but you omit that data field during extraction?

Instead of letting your application crash randomly at runtime with a cryptic Python error, pydantic-pick uses Abstract Syntax Tree (AST) parsing to peek inside your methods and wrappers.

It maps exactly which self attributes your functions access. If a method relies on a field that you omitted, pydantic-pick gracefully and silently omits the method as well! This cascades, so if method_b relies on method_a, and method_a was dropped, method_b is safely dropped too.

Clean Developer Experience Errors

If another developer on your team tries to call a method or field that was dynamically dropped, pydantic-pick intercepts it via a custom __getattr__ and provides a clear error:

PublicUser = pick_model(DBUser, ("id", "username"), "PublicUser")
user = PublicUser(id=1, username="alice")

user.check_password("secret")

Output:

AttributeError: 'PublicUser' object has no attribute 'check_password'.
-> This field/method was intentionally omitted by pydantic-pick during extraction.

Function Reference

from pydantic_pick import pick_model, omit_model

# Keep only specified fields
pick_model(
    base: Type[BaseModel],      # Your original model class
    paths: tuple[str, ...],     # Fields to keep (dot-notation for nested)
    name: str                   # Name for the new model class
) -> Type[BaseModel]

# Remove specified fields, keep the rest
omit_model(
    base: Type[BaseModel],      # Your original model class
    paths: tuple[str, ...],     # Fields to remove (dot-notation for nested)
    name: str                   # Name for the new model class
) -> Type[BaseModel]

The paths parameter uses dot-notation for nested fields (e.g., "profile.settings.theme").

Truthful Limitations & Quirks

Because dynamic AST generation and Pydantic's Rust-based core have strict boundaries, there are a few edge cases this library does not currently handle. Be aware of these before using it in production:

Warning: Both @model_validator and @model_serializer are intentionally ignored during extraction. Because mode="before" model validators check dictionary state rather than self.attribute state, our AST parser cannot reliably map their dependencies. Copying them to a subset class where fields might be missing would cause fatal dictionary/Attribute errors at runtime, so pydantic-pick safely drops them.

  1. Forward References: If you use string-based forward references for circular imports (e.g., leader: "User"), the extraction engine cannot peek inside the string to extract nested fields.
  2. Private Attributes: PrivateAttr() definitions are currently lost during extraction.
  3. Field Aliases in Paths: When defining your include paths, you must use the actual internal Python variable name, not the Pydantic alias. (e.g., Use "first_name", not "firstName").
  4. Sets and model_dump: If you extract a model containing a Set[NestedModel], remember that Pydantic V2 requires you to use model_dump(mode="json") to serialize sets. Standard model_dump() will throw a standard Python TypeError: unhashable type: 'dict'.
  5. Generic Models: Dynamically creating a subset of a Generic[T] model results in a standard model; it will lose its generic subscriptable properties.

Links

License

MIT License

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydantic_pick-0.2.0.tar.gz (33.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydantic_pick-0.2.0-py3-none-any.whl (10.5 kB view details)

Uploaded Python 3

File details

Details for the file pydantic_pick-0.2.0.tar.gz.

File metadata

  • Download URL: pydantic_pick-0.2.0.tar.gz
  • Upload date:
  • Size: 33.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.6

File hashes

Hashes for pydantic_pick-0.2.0.tar.gz
Algorithm Hash digest
SHA256 3757a266dacad4dd5f8bb0a9cd15f65a9166742319955f1828b68b7f98175bba
MD5 b98c67bcb7b366ab99fc131cb950e5f1
BLAKE2b-256 505247c2b5be5403b9c27842cb90b4ce0ae7d71f8199792eeb4e94c2e43b49df

See more details on using hashes here.

File details

Details for the file pydantic_pick-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: pydantic_pick-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 10.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.6

File hashes

Hashes for pydantic_pick-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e6050c4bdd816f2f84f6210da1a70fca73eff3250f54e24997a5deef677834ef
MD5 b45c67ef2726a52913ff8c67425acbe4
BLAKE2b-256 653d8f5f4c1071d10def745e6f8961c2dece963580ddf7435efcd6c476ffb225

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page