Skip to main content

Pydantic partial model class, with ability to easily dynamically omit fields when serializing a model.

Project description

from pydantic import BaseModel

Pydantic Partials

Adds extra features using the new built-in MISSING feature from Pydantic.

  • An easy way to add or create automatically make all fields on a model partial-fields.
    • For a Pydantic model via it's new MISSING feature.
    • Makes all fields on a subclass automatically have the MISSING type annotation on them.
    • All required fields (ie: no default value ) will have their default value set to MISSING.
      • Makes all fields not required.
  • Optional patch to MISSING to make it falsy
    • You have to enable this explicitly, it's opt-in.

This can be handy when you want to take an existing class that has required fields and make them all not required.

You can subclass the model and the subclass can automatically make all of it's required fields (ie: fields that don't have a default value), instead default them to MISSING.

This makes it so all fields are not required anymore, and you can use this subclass in (for example) an API endpoint that you want PATCH-like behavior (where you can individually update specific fields on already existing model data).

PythonSupport

Documentation

📄 Detailed Documentation | 🐍 PyPi

Important Upgrade from v2.x to 3.x Notes

Switched to using the built-in MISSING value from Pydantic. 3.x is fully backwards compatible with 2.x except that Mising is now truthy (where it previously was falsy).

That means the Missing value is exactly the same as Pydantic's MISSING value. I kept the old name in place for backwards compatability, and for anyone who likes that capitalization better.

However, I have an option you can enable that can make it truly 100% backwards compatbile/non-breaking:

If you call the patch_missing_to_make_falsy function it will patch MISSING to be falsy, and therefore Missing is falsey (MISSING and Missing are both the same exact value), like this:

from pydantic_partials import patch_missing_to_make_falsy

patch_missing_to_make_falsy()

You may want MISSING to be falsey even if you don't need the backwards compatability.

Since the patching is to a global type MISSING, it's opt-in only via patch_missing_to_make_falsy and not automatically applied.

Quick Start

Install

poetry install pydantic-partials

or

pip install pydantic-partials

Introduction

You can create from scratch, or convert existing models to be Partials. The main purpose will be to add to exiting models, and hence the default behavior of making all non-default fields partials (configurable).

Two Partial Base Class Options

There are two options to inherit from:

  • PartialModel
    • With this one, you must explicitly set which fields are partial
    • To get correct static type checking, you also can also set a partial field's default value to Missing.
  • AutoPartialModel
    • This automatically applies partial behavior to every attribute that does not already have a default value.

Let's first look at a basic automatically defined partials example.

Automatically Defined Partials - Basic Example

Very basic example of a simple model with automatically defined partial fields, follows:

from pydantic_partials import AutoPartialModel, Missing

class MyModel(AutoPartialModel):
    some_attr: str
    another_field: str

# By default, automatic defined partial fields without any value will get set to a
# special `Missing` type. Any field that is set to Missing is
# excluded from the model_dump/model_dump_json.
obj = MyModel()
assert obj.some_attr is Missing
assert obj.model_dump() == {}

# You can set the real value at any time, and it will behave like expected.
obj.some_attr = 'hello'
assert obj.some_attr is 'hello'
assert obj.model_dump() == {'some_attr': 'hello'}

# You can always manually set a field to `Missing` directly.
obj.some_attr = Missing

# And now it's removed from the model-dump.
assert obj.model_dump() == {}

# The json dump is also affected in the same way.
assert obj.model_dump_json() == '{}'

# Any non-missing fields will be included when dumping/serializing model.
obj.another_field = 'assigned-value'

# After dumping again, we have `another_field` outputted.
# The `some_attr` field is not present since it's still `Missing`.
assert obj.model_dump() == {'another_field': 'assigned-value'}

By default, all fields without a default value will have the ability to be partial, and can be missing from both validation and serialization. This includes any inherited Pydantic fields (from a superclass).

More Details

Missing

The Missing value is a sentinel, and there is never more than one instance of it. So you can use the is operator with it, just like you would with None. It's of type MissingType.

When evaluated as a bool, Missing is always False; just like how None evaluates to False.

Inheritable

With AutoPartialModel, you can inherit from a model to make an automatic partial-version of the inherited fields:

from pydantic_partials import AutoPartialModel, Missing
from pydantic import ValidationError, BaseModel

class TestModel(BaseModel):
    name: str
    value: str
    some_null_by_default_field: str | None = None

try:
    # This should produce an error because
    # `name` and `value`are required fields.
    TestModel()
except ValidationError as e:
    print(f'Pydantic will state `name` + `value` are required: {e}')
else:
    raise Exception('Pydantic should have required `required_decimal`.')

    # We inherit from `TestModel` and add `PartialModel` to the mix.

class PartialTestModel(AutoPartialModel, TestModel):
    pass

# `PartialTestModel` can now be allocated without the required fields.
# Any missing required fields will be marked with the `Missing` value
# and won't be serialized out.
obj = PartialTestModel(name='a-name')

assert obj.name == 'a-name'
assert obj.value is Missing
assert obj.some_null_by_default_field is None

# The `None` field value is still serialized out,
# only fields with a `Missing` value assigned are skipped.
assert obj.model_dump() == {
    'name': 'a-name', 'some_null_by_default_field': None
}

Notice that if a field has a default value, it's used instead of marking it as Missing.

Also, the Missing sentinel value is a separate value vs None, allowing one to easily know if a value is truly just missing or is None/Null.

Exclude Fields from Automatic Partials (AutoPartialModel)

You can exclude specific fields from the automatic partials via these means:

  • AutoPartialExclude[...]
    • This puts a special Annotated item on field to mark it as excluded.
  • class PartialRequired(PartialModel, auto_partials_exclude={'id', 'created_at'}):
    • This way provides them via class argument auto_partials_exclude
  • Or via the standard model_config
    • model_config = {'auto_partials_exclude': {'id', 'created_at'}}
    • A dict, using auto_partials_exclude as the key and a set of field names as the value.

Any of these methods are inheritable. You can override an excluded value by explicitly marking a field as Partial via some_field: Partial[str]

Here is an example using the AutoPartialExclude method, also showing how it can inherit.

from pydantic_partials import AutoPartialModel, AutoPartialExclude, Missing
from pydantic import BaseModel, ValidationError
from datetime import datetime
import pytest

class PartialRequired(AutoPartialModel):
    id: AutoPartialExclude[str]
    created_at: AutoPartialExclude[datetime]

class TestModel(BaseModel):
    id: str
    created_at: datetime
    name: str
    value: str
    some_null_by_default_field: str | None = None

class PartialTestModel(TestModel, PartialRequired):
    pass

# Will raise validation error for the two fields excluded from auto-partials
with pytest.raises(
    ValidationError,
    match=r'2 validation errors[\w\W]*'
          r'id[\w\W]*Field required[\w\W]*'
          r'created_at[\w\W]*Field required'
):
    # This should raise a 'ValidationError'
    PartialTestModel()  # type: ignore

# If we give them values, we get no ValidationError
obj = PartialTestModel(id='some-value', created_at=datetime.now())  # type: ignore

# And fields have the expected values.
assert obj.id == 'some-value'
assert obj.name is Missing

Auto Partials Configuration

Normally you would simply inherit from either PartialModel or AutoPartialModel, depending on the desired behavior you want.

But you can also configure the auto-partials aspect via class paramters or the model_config attribute:

from pydantic_partials import PartialModel, PartialConfigDict, AutoPartialModel

# `PartialModel` uses `auto_partials` as `False` by default, but we can override that if you want via class argument:
class TestModel1(PartialModel, auto_partials=True):
    ...

# Or via `model_config`
# (PartialConfigDict inherits from Pydantic's `ConfigDict`,
#  so you have all of Pydantic's options still available).
class TestModel2(AutoPartialModel):
    model_config = PartialConfigDict(auto_partials=False)
    ...

You can disable this automatic function. This means you have complete control of exactly which field can be partial or not. You can use either the generic Partial[...] generic or a union with MissingType to mark a field as a partial field. The generic simple makes the union to MissingType for you.

from pydantic_partials import PartialModel, Missing, MissingType, Partial
from decimal import Decimal
from pydantic import ValidationError

class TestModel(PartialModel):
    # Can use `Partial` generic type
    partial_int: Partial[int] = Missing

    # Or union with `MissingType`
    partial_str: str | MissingType

    required_decimal: Decimal

try:
    TestModel()
except ValidationError as e:
    print(f'Pydantic will state `required_decimal` is required: {e}')
else:
    raise Exception('Pydantic should have required `required_decimal`.')

obj = TestModel(required_decimal='1.34')

# You can find out at any time if a field is missing or not:
assert obj.partial_int is Missing
assert obj.partial_str is Missing

assert obj.required_decimal == Decimal('1.34')

Explicitly Defined Partials - Basic Example

Pydantic now supports the basic non-automatic style out of the box via MISSING, ie:

from pydantic import BaseModel
from pydantic_core import MISSING

class MyModel(BaseModel):
    some_field: str
    partial_field: str | MISSING = MISSING
    

Previous to Pydantic v3.12, it had no MISSING feature and so the explicitly defeined partials was the way to do it.

I've left it in for two reasons:

  1. Backwards compatability.
  2. It will still for any required (ie: no default value define) set the fields default value of any MISSING typed-fields to the MISSING value.
    • This aspect is still might come in handy, depending on the situation.

Examples

Very basic example of a simple model with explicitly defined partial fields, follows:

from pydantic_partials import PartialModel, Missing, Partial, MissingType
from pydantic import ValidationError

class MyModel(PartialModel):
    some_field: str
    partial_field: Partial[str] = Missing
    
    # Alternate Syntax:
    alternate_syntax_partial_field: str | MissingType = Missing
    

# By default, `Partial` fields without any value will get set to a
# special `Missing` type. Any field that is set to Missing is
# excluded from the model_dump/model_dump_json.
obj = MyModel(some_field='a-value')
assert obj.partial_field is Missing
assert obj.model_dump() == {'some_field': 'a-value'}

# You can set the real value at any time, and it will behave like expected.
obj.partial_field = 'hello'
assert obj.partial_field == 'hello'
assert obj.model_dump() == {'some_field': 'a-value', 'partial_field': 'hello'}

# You can always manually set a field to `Missing` directly.
obj.partial_field = Missing

# And now it's removed from the model-dump.
assert obj.model_dump() == {'some_field': 'a-value'}

# The json dump is also affected in the same way.
assert obj.model_dump_json() == '{"some_field":"a-value"}'

try:
    # This should produce an error because
    # `some_field` is a required field.
    MyModel()
except ValidationError as e:
    print(f'Pydantic will state `some_field` is required: {e}')
else:
    raise Exception('Pydantic should have required `some_field`.')

Important Upgrade from v1.x to 2.x Notes

I decided to make the default behavior of PartialModel not be automatic anymore.

I made a new class named AutoPartialModel that works exactly the same as the old v1.x PartialModel previously did.

To upgrade, simply replace PartialModel with AutoPartialModel, and things will work exactly as they did before. The auto_partials configuration option is still present and if present will still override the base-class setting.

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_partials-3.0.0.tar.gz (14.0 kB view details)

Uploaded Source

Built Distribution

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

pydantic_partials-3.0.0-py3-none-any.whl (12.7 kB view details)

Uploaded Python 3

File details

Details for the file pydantic_partials-3.0.0.tar.gz.

File metadata

  • Download URL: pydantic_partials-3.0.0.tar.gz
  • Upload date:
  • Size: 14.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.1 CPython/3.12.3 Linux/6.11.0-1018-azure

File hashes

Hashes for pydantic_partials-3.0.0.tar.gz
Algorithm Hash digest
SHA256 9a4d9bf98cab8246d027286a3663c26421dc097f503752bbb7f80ba911c5837f
MD5 79f3a6ea5501e5b12855c79035002829
BLAKE2b-256 56737263e14750ec2ab9f84723469a085f66462589fda9dad253d6008a8c03e0

See more details on using hashes here.

File details

Details for the file pydantic_partials-3.0.0-py3-none-any.whl.

File metadata

  • Download URL: pydantic_partials-3.0.0-py3-none-any.whl
  • Upload date:
  • Size: 12.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.1 CPython/3.12.3 Linux/6.11.0-1018-azure

File hashes

Hashes for pydantic_partials-3.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4d6ce8ab8bd5ec52453b7db315a669c23e5f2a188db95c5ac618345b3ec37e76
MD5 098a6ace30c17298535bb57eede7f2b3
BLAKE2b-256 c165b94bf3126505692b79a2cbc68d64c4af676517fddcf69b034f321a873b90

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