Dynamically extract and subset Pydantic V2 models using dot-notation while preserving validators.
Project description
pydantic-pick
Dynamically create subsets of Pydantic V2 models using
pick_modeloromit_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(...)(likege,max_length,alias). - ✅ Field Validators:
@field_validatorlogic is preserved (as long as the fields it targets were not omitted). - ✅ Computed Fields:
@computed_fieldproperties are safely carried over. - ✅ Methods: Custom instance methods,
@classmethod,@staticmethod, and custom wrappers. - ✅ ClassVars:
typing.ClassVarattributes are safely mapped. - ✅ Config: Your
model_config(likefrozen=Trueoralias_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.
- 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. - Private Attributes:
PrivateAttr()definitions are currently lost during extraction. - 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"). - Sets and
model_dump: If you extract a model containing aSet[NestedModel], remember that Pydantic V2 requires you to usemodel_dump(mode="json")to serialize sets. Standardmodel_dump()will throw a standard PythonTypeError: unhashable type: 'dict'. - Generic Models: Dynamically creating a subset of a
Generic[T]model results in a standard model; it will lose its generic subscriptable properties.
Links
- GitHub: https://github.com/StoneSteel27/pydantic-pick
- Issues: https://github.com/StoneSteel27/pydantic-pick/issues
License
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3757a266dacad4dd5f8bb0a9cd15f65a9166742319955f1828b68b7f98175bba
|
|
| MD5 |
b98c67bcb7b366ab99fc131cb950e5f1
|
|
| BLAKE2b-256 |
505247c2b5be5403b9c27842cb90b4ce0ae7d71f8199792eeb4e94c2e43b49df
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e6050c4bdd816f2f84f6210da1a70fca73eff3250f54e24997a5deef677834ef
|
|
| MD5 |
b45c67ef2726a52913ff8c67425acbe4
|
|
| BLAKE2b-256 |
653d8f5f4c1071d10def745e6f8961c2dece963580ddf7435efcd6c476ffb225
|