ProvenPin Install

beartype conf + pydantic model_validate runtime checks: working versions (tested 2026-10-16)

Pair beartype boundary checks with pydantic model_validate, using BeartypeConf to align int/float coercion and route violations, and pin down the gotchas where the two guards disagree.

passed5 passed · 0 failed2026-10-16 00:01 UTC
Status
passed
Last passed
2026-10-16 00:01 UTC
Tests
5 / 5
Language
Python
Runtime
Python 3.13.16
pinned versions3
beartype@0.23.1pydantic@2.14.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38826982604

The code

Exactly the files that passed. Your agent can pull them with get_recipe("beartype-pydantic-runtime-check") (install), or see them on GitHub.

requirements.txt
beartype==0.23.1
pydantic==2.14.1
pytest==9.1.1
checks.py
from beartype import BeartypeConf, beartype
from pydantic import BaseModel, ConfigDict, PositiveInt, TypeAdapter

# beartype rejects int where float is annotated, while pydantic coerces int to
# float. Enabling the PEP 484 tower makes beartype accept ints for floats.
TOWER = BeartypeConf(is_pep484_tower=True)


class SchemaError(TypeError):
    pass


# Route every beartype violation through one app exception so callers can catch
# schema errors with a single except clause alongside pydantic.ValidationError.
APP = BeartypeConf(is_pep484_tower=True, violation_param_type=SchemaError)


class Account(BaseModel):
    # revalidate_instances forces model_validate to re-check instances that
    # bypassed validation through model_construct.
    model_config = ConfigDict(revalidate_instances="always", extra="forbid")
    name: str
    balance: PositiveInt
    rate: float = 0.0


def parse_account(raw: object) -> Account:
    # The only place that coerces and enforces constraints such as PositiveInt.
    return Account.model_validate(raw)


positive_int = TypeAdapter(PositiveInt)


@beartype
def snapshot(account: Account) -> str:
    # beartype only checks the runtime class, it never builds an Account.
    return f"{account.name}:{account.balance}"


@beartype
def rate_of(rate: float) -> float:
    return rate


@beartype(conf=TOWER)
def rate_of_lax(rate: float) -> float:
    return rate


@beartype
def nominal(amount: PositiveInt) -> int:
    # beartype shaves Annotated down to int and drops the Gt(0) metadata.
    return amount


@beartype(conf=APP)
def credit(account: Account, amount: int) -> int:
    return account.balance + amount
tests/test_checks.py
import pytest
from beartype.roar import BeartypeCallHintParamViolation
from pydantic import ValidationError

from checks import (
    Account,
    SchemaError,
    credit,
    nominal,
    parse_account,
    positive_int,
    rate_of,
    rate_of_lax,
    snapshot,
)


def test_pydantic_coerces_but_beartype_does_not():
    account = parse_account({"name": "ada", "balance": "5"})
    assert account.balance == 5
    with pytest.raises(BeartypeCallHintParamViolation):
        snapshot({"name": "ada", "balance": 5})


def test_pep484_tower_matches_pydantic_int_to_float():
    account = parse_account({"name": "ada", "balance": 1, "rate": 2})
    assert account.rate == 2.0
    with pytest.raises(BeartypeCallHintParamViolation):
        rate_of(2)
    assert rate_of_lax(2) == 2


def test_beartype_drops_pydantic_constraint_metadata():
    assert nominal(-5) == -5
    with pytest.raises(ValidationError):
        positive_int.validate_python(-5)


def test_app_conf_routes_violations_to_schema_error():
    account = parse_account({"name": "ada", "balance": 1})
    with pytest.raises(SchemaError):
        credit(account, "10")


def test_model_construct_bypasses_both_until_revalidated():
    sneaky = Account.model_construct(name="ada", balance=-1)
    assert snapshot(sneaky) == "ada:-1"
    with pytest.raises(ValidationError):
        parse_account(sneaky)
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.