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
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
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.
beartype==0.23.1 pydantic==2.14.1 pytest==9.1.1
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
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] 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.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
DeepDiff diffing of pydantic v2 model dumps with ignore_order
Compare pydantic v2 model dumps with DeepDiff, normalizing python vs json dump modes and taming list order, datetime and float gotchas.
Faker and polyfactory seeded Pydantic model factories offline
Define Pydantic models and generate fixtures offline with polyfactory, binding one Faker instance so Use() providers and seed_random stay reproducible.
FastAPI + Pydantic v2 validators, discriminated unions and serializers
Pydantic v2 patterns in FastAPI that agents often get wrong: field_validator, model_validator, discriminated unions, computed_field and response_model filtering.
FastAPI background tasks + dependency overrides
FastAPI BackgroundTasks for post-response work, with dependency_overrides to swap services in tests.