Skip to content
Harness Engineering2026-09-1012 min read

Pydantic Strict Validation: Choosing What an Input May Become

Learn Pydantic strict validation to decide which conversions a quantity field should accept, and which ones deserve a clear rejection.

Key takeaways

  • Python's isinstance(True, int) is true, so a hand-written type check silently admits Boolean values into integer fields.
  • Pydantic strict mode rejects Boolean, float and numeric-string alternatives for a field typed as an integer, closing that gap.
  • Coercing mode and strict mode answer different questions: whether a value can become an integer versus whether it already is one.
  • A field's business contract, not Python's type hierarchy, should decide whether a converted value counts as acceptable input.
  • Rejecting a legacy numeric-string client outright is a migration decision that needs an owner, not a silent coercion fix.

Rod Rivera

Author

Pydantic Strict Validation: Choosing What an Input May Become

Rod's note — read with a pencil; the margins are for you.

The reply says six, but what type is six

A supplier integration receives a replenishment draft: {"sku": "vanilla", "quantity": "6"}. A person reading that JSON sees the number six. Python sees a string. If a downstream function multiplies quantity by two expecting an integer, it gets "66", not 12, because string repetition and integer multiplication share an operator but not a meaning. The bug is not that six was ambiguous. The bug is that nobody decided, in code, what type of six the program was willing to accept.

This is a decision every application boundary has to make, and it gets harder, not easier, once an LLM is producing the input. A model can be told in a prompt to return an integer. The receiving program still needs to check the returned value, and the field that receives the model's output still has to answer a narrower question: given this specific value, in this specific shape, does it satisfy the contract this field represents? That question is answered by runtime validation, not by hoping the sender behaved.

Why isinstance is not enough

Python's own type system contains a trap that makes hand-rolled validation unreliable. bool is a subclass of int. This is a real, longstanding feature of the language: True behaves as 1 and False behaves as 0 in arithmetic, and isinstance(True, int) evaluates to True. For most everyday code that is harmless. For a validation function that checks isinstance(value, int) before accepting a quantity, it means a Boolean sails through the same door as an integer.

Consider a field called quantity on an order line, with a business rule that it must represent a countable number of tubs, from 1 to 50. If a model, a form, or an upstream system sends True for that field, an isinstance check alone will say yes. But True is not a quantity. Nobody orders True tubs of vanilla. The type system's internal convenience, useful for arithmetic, is the wrong lens for a business contract about physical inventory.

A useful specification for this field says which representations are accepted before any stock calculation runs. For example: accept a Python integer from 1 through 50; reject Boolean values, floats and strings. A reviewer can now check the implementation against an explicit list rather than interpret what the author meant by "number."

What strict mode actually changes

Pydantic, the widely used Python data-validation library, offers a mode that closes this gap directly. Pydantic's strict-mode documentation describes reduced coercion and type-dependent differences between Python and JSON input. That distinction between Python input and JSON input matters more than it first appears, and the worked example below treats it as a separate question rather than folding it into the integer discussion.

In Pydantic's default, non-strict mode, a field typed quantity: int will happily convert a numeric string like "6" into the integer 6. That conversion can be exactly what you want at some boundaries, such as a web form where every field arrives as text. Strict mode turns that convenience off. A strict integer field accepts only an actual integer. It does not accept "6", does not accept 6.0, and does not accept True, The float and string can represent six; the Boolean represents a truth value.

An existing course lesson on runtime contracts works through this precisely: a strict integer quantity contract rejects Boolean, float, and numeric-string alternatives, and the lesson treats this as a demonstration of a policy choice rather than a new production measurement (From Prompt Restrictions to Runtime Contracts with Pydantic). That framing is the one worth carrying forward here: strict mode is not a universal correctness switch. It is a decision about which conversions this particular field is allowed to perform, made explicit and enforced consistently instead of left to whichever code path happens to touch the value first.

Working the six candidate inputs all the way through

Take a strict integer field with a valid range of 1 through 50, representing a tub quantity. Before running anything, predict what each of the following Python values should do against that field.

Python valueTypeExpected result under strict modeReason
6intAcceptAn integer within range
TrueboolRejectSubclass of int, but not a counted quantity
6.0floatRejectA float, even one with no fractional part, is not an integer type
'6'strRejectA numeric string is text, not a number, until something converts it
0intRejectBelow the field's stated minimum of 1
51intRejectAbove the field's stated maximum of 50

Five of the six candidate values are rejected. Only 6 satisfies both requirements: it has the accepted type and falls inside the declared bounds. Nothing here is a claim about a new execution or a fresh benchmark. It is a table you can check by hand against Pydantic's own documented behavior for strict integer fields, and against the existing course example that walks through the same six values (From Prompt Restrictions to Runtime Contracts with Pydantic).

The second half of this worked case is easy to skip past, and skipping it is where mistakes happen. JSON has no native concept of a Python bool subclassing int, and it represents numbers, strings, and booleans as distinct token types at the syntax level. That does not mean every field's behavior is identical once you serialize these same six values to JSON text and validate from that JSON rather than from a live Python dictionary. For this strict integer field, the six cases should have the same accept/reject outcomes through either input path. Other types need their own checks. The instructive move is not to assume the Python-input table above transfers unchanged to a JSON-input path. It is to treat that as a separate, second check against the specific field and installed library version, rather than a generalization from one type to every type.

A flow diagram showing a Python value moving through a strict Pydantic integer field, with True, 6.0 and the string 6 all rejected while the integer 6 passes through to the business rule stage.

Where a candidate value gets rejected
Recheck itemRecorded basisWhat to verify locally
Strict integer conversion rulesPydantic documentation checked 10 September 2026Installed version and field configuration
Python versus JSON inputThe six-value fixture aboveBoth entry points used by your application
Business rangeHypothetical inventory contractYour actual minimum, maximum and ownership of changes

Deciding the policy before writing the field

The practical task is not memorizing that Booleans get rejected. It is deciding, for each field in your application, what its contract actually represents, and then encoding that decision so it does not depend on which code path happens to touch the value first. A tub quantity is a countable, positive whole number with an upper bound tied to some operational limit, such as truck capacity or shelf space. That is a specific claim about the business, not a fact about Python's numeric types, and writing the bound down, as an explicit ge=1, le=50 style constraint, makes the decision inspectable by the next person who reads the code.

First, name the contract in business terms

Write down, in a sentence a non-programmer could check, what the field represents: a countable quantity, a currency amount, an identifier. Do this before picking a Pydantic type.

Second, choose strict or coercing per field

Decide whether this specific field should accept a numeric string or a Boolean-shaped value as a convenience, or whether that conversion papers over a client that is sending the wrong shape. Strict and coercing modes answer different questions; pick deliberately rather than by default.

Third, test the rejected values, not just the accepted one

Confirm the field rejects the values it is supposed to reject. A field that only ever sees valid input in testing has not been tested against the argument it exists to make.

Fourth, decide what a rejection tells the caller

A validation failure should produce a message a client, or a client's developer, can act on. Silently coercing a legacy input is a different choice than rejecting it with a clear reason, and the two have different consequences for anyone still sending the old shape.

Strict mode is not a universal correctness switch

Turning on strict validation for an integer field tells you nothing about whether a date field, a currency field, or a nested list behaves the same way. Pydantic's documented exceptions for date and time types mean each field type needs its own check against the current version, not an assumption carried over from the integer case.

The migration question strict mode forces

Turning on strict validation for an existing field is not free. If a client has been sending quantity as the string "6" for months, and that string has been silently coerced into the integer 6 the whole time, flipping the field to strict mode will start rejecting every request from that client, immediately. This is exactly the situation that separates a good validation change from a disruptive one: the fix that closes the Boolean-quantity gap is the same fix that breaks a legitimate but loosely typed caller.

Measure that compatibility change before enabling it. Before enabling strict rejection in a live path, replay a sample of real historical-shaped payloads through both the old, permissive parser and the new, strict one, in an observation-only mode that does not affect any live decision. Keep the original payload, which parser version processed it, and which rejection code the strict parser would have produced. The check that matters here is narrow: valid integers like 6 must keep passing under the new parser exactly as they did under the old one. If that check fails, something in the new configuration is wrong, and the rollout should not proceed. A numeric string like "6" failing under the new, strict parser is the expected and intended difference, not a bug, since that was the entire point of tightening the field.

Once the comparison confirms the expected split, the operational decision belongs to whoever owns the API contract, not to the validation library. Legacy clients still sending numeric strings need a specific answer: are they migrated to send true integers, or do they receive a clear rejection message pointing at the field and the expected type? Silently coercing them forever means the strict field was never actually strict, just strict-looking. Routing them to an explicit migration notice, naming the affected client versions, the exact error response they will see, and who owns the cutover, turns a type-system detail into a manageable operational change instead of a surprise outage.

Quick check — A field is typed as a strict integer with range 1 to 50. A client sends the JSON value true for that field. What happens?

Check the decision

What would show this reasoning is wrong

This account rests on a specific, checkable claim: that Pydantic's strict mode, applied to an integer field, rejects Boolean, float, and numeric-string values while accepting integers within a declared range. If you configure a strict integer field with the current stable release of Pydantic and find that a Boolean value is accepted without error, that would contradict the behavior described here and in the existing course example, and would mean either the library's default configuration changed or the field was not actually configured as strict. Matching outcomes for this integer field are expected. They do not contradict the documented exceptions for other types. Add a separate date-field test if your model also accepts dates, using the exact Python and JSON entry points your application calls. Any of these results would mean rereading the current documentation and retesting against the specific field and version in front of you, since a validation library's coercion rules are the kind of detail that can shift between releases.

The usable next step is not to trust this table from memory. Take the field you are actually building, write its business contract in one sentence, choose strict or coercing mode for a stated reason, and run the six-value check yourself before wiring it to anything that spends money or moves inventory.

Work through the full runtime-contracts lesson and its business-rule follow-up

Continue with the course lesson that builds this same strict-integer contract into a nested draft object and connects it to inventory checks beyond schema validation alone.

Ready to put an agent to work?

Join the Prof Rod newsletter for one educational lesson a week, with worked examples attached. It is free to register for and separate from the Zero Employee community.