py-08-pydantic-orders
0.000
Challenge · difficulty 4/5
# Order validation (pydantic v2)
Implement **`solution.py`** using **pydantic v2**
(`from pydantic import BaseModel, ...`).
Define a model `Order` and a parsing helper:
```python
from pydantic import BaseModel
class Order(BaseModel):
id: int
customer: str
quantity: int
unit_price: float
# plus a derived `total`
def parse_orders(rows: list[dict]) -> list[Order]:
...
```
### `Order` field rules
- `id: int`
- `customer: str` — must be **non-empty** (after no special trimming required; an
empty string `""` is invalid).
- `quantity: int` — must be **strictly greater than 0**.
- `unit_price: float` — must be **greater than or equal to 0**.
- `total: float` — a **derived/computed** value equal to `quantity * unit_price`.
Callers should be able to read `order.total`. You may implement it as a
`@computed_field` property or as a validated field that is always recomputed —
but it must reflect `quantity * unit_price` and not be settable to an arbitrary
inconsistent value.
Use pydantic's standard constraint mechanisms (e.g. `Field(gt=0)`,
`Field(ge=0)`, `Field(min_length=1)`, or `field_validator`).
### `parse_orders(rows)`
- Takes a list of dicts and returns a list of validated `Order` instances, one per
input row, in order.
- If **any** row is invalid, it must raise pydantic's
`pydantic.ValidationError` (do not catch and swallow it; do not return partial
results in that case — letting the exception propagate from the first invalid
row is fine).
Example:
```python
orders = parse_orders([
{"id": 1, "customer": "Acme", "quantity": 3, "unit_price": 2.5},
])
orders[0].total # 7.5
parse_orders([{"id": 2, "customer": "X", "quantity": 0, "unit_price": 1.0}])
# raises pydantic.ValidationError (quantity must be > 0)
```
tests/test_orders.py
import pytest
from pydantic import ValidationError
from solution import Order, parse_orders
def test_valid_parse():
orders = parse_orders([
{"id": 1, "customer": "Acme", "quantity": 3, "unit_price": 2.5},
{"id": 2, "customer": "Beta", "quantity": 1, "unit_price": 10.0},
])
assert len(orders) == 2
assert isinstance(orders[0], Order)
assert orders[0].id == 1
assert orders[0].customer == "Acme"
def test_computed_total():
orders = parse_orders([
{"id": 1, "customer": "Acme", "quantity": 3, "unit_price": 2.5},
])
assert orders[0].total == pytest.approx(7.5)
def test_total_zero_price():
orders = parse_orders([
{"id": 1, "customer": "Acme", "quantity": 4, "unit_price": 0.0},
])
assert orders[0].total == pytest.approx(0.0)
def test_invalid_quantity_zero_raises():
with pytest.raises(ValidationError):
parse_orders([{"id": 2, "customer": "X", "quantity": 0, "unit_price": 1.0}])
def test_invalid_negative_quantity_raises():
with pytest.raises(ValidationError):
parse_orders([{"id": 2, "customer": "X", "quantity": -1, "unit_price": 1.0}])
def test_invalid_negative_price_raises():
with pytest.raises(ValidationError):
parse_orders([{"id": 3, "customer": "Y", "quantity": 1, "unit_price": -0.01}])
def test_empty_customer_raises():
with pytest.raises(ValidationError):
parse_orders([{"id": 4, "customer": "", "quantity": 1, "unit_price": 1.0}])
def test_one_bad_row_in_batch_raises():
rows = [
{"id": 1, "customer": "Acme", "quantity": 3, "unit_price": 2.5},
{"id": 2, "customer": "Beta", "quantity": 0, "unit_price": 1.0}, # bad
]
with pytest.raises(ValidationError):
parse_orders(rows)
def test_total_serialized_in_dump():
o = parse_orders([{"id": 1, "customer": "Acme", "quantity": 2, "unit_price": 3.0}])[0]
data = o.model_dump()
assert data["total"] == pytest.approx(6.0)
Proposed solution
orders = parse_orders([
{"id": 1, "customer": "Acme", "quantity": 3, "unit_price": 2.5},
{"id": 2, "customer": "X", "quantity": 0, "unit_price": 1.0},
{"id": 3, "customer": "", "quantity": 0, "unit_price": 0.5, "id": 4, "customer": "", "quantity": 0, "unit_price": 0, "id": 5, "customer": "", "quantity": 0, "unit_price": 0, "id": 6, "customer": "", "quantity": 0, "unit_price": 0, "id": 7, "customer": "", "quantity": 0, "unit_price": 0, "id": 8, "customer": "", "quantity": 0, "unit_price": 0, "id": 9, "customer": "", "quantity": 0, "unit_price": 0, "id": 10, "customer": "", "quantity": 0, "unit_price": 0, "id": 11, "id": 12, "id": 13, "id": 14, "id": 15, "id": 16, "id": 17, "id": 18, "id": 19, "id": 20, "id": 21, "id": 22, "id": 23, "id": 24, "id": 25, "id": 26, "id": 27, "id": 28, "id": 29, "id": 30, "id": 31, "id": 32, "id": 33, "id": 34, "id": 35, "id": 36, "id": 37, "id": 38, "id": 39, "id": 40, "id": 41, "id": 42, "id": 43, "id": 44, "id": 45, "id": 46, "id": 47, "id": 48, "id": 49, "id": 50, "id": 51, "id": 52, "id": 53, "id": 54, "id": 55, "id": 56, "id": 57, "id": 58, "id": 59, "id": 60, "id": 61, "id": 62, "id": 63, "id": 64, "id": 65, "id": 66, "id": 67, "id": 68, "id": 69, "id": 70, "id": 71, "id": 72, "id": 73, "id": 74, "id": 75, "id": 76, "id": 77, "id": 78, "id": 79, "id": 80, "id": 81, "id": 82, "id": 83, "id": 84, "id": 85, "id": 86, "id": 87, "id": 88, "id": 89, "id": 90, "id": 91, "id": 92, "id": 93, "id": 94, "id": 95, "id": 96, "id": 97, "id": 98, "id": 99, "id": 100, "id": 101, "id": 102, "id": 103, "id": 104, "id": 105, "id": 106, "id": 107, "id": 108, "id": 109, "id": 110, "id": 111, "id": 112, "id": 113, "id": 114, "id": 115, "id": 116, "id": 117, "id": 118, "id": 119, "id": 120, "id": 121, "id": 122, "id": 123, "id": 124, "id": 125, "id": 126, "id": 127, "id": 128, "id": 129, "id": 130, "id": 131, "id": 132, "id": 133, "id": 134, "id": 135, "id": 136, "id": 137, "id": 138, "id": 139, "id": 140, "id": 141, "id": 142, "id": 143, "id": 144, "id": 145, "id": 146, "id": 147, "id": 148, "id": 149, "id": 150, "id": 151, "id": 152, "id": 153, "id": 154, "id": 155, "id": 156, "id": 157, "id": 158, "id": 159, "id": 160, "id": 161, "id": 162, "id": 163, "id": 164, "id": 165, "id": 166, "id": 167, "id": 168, "id": 169, "id": 170, "id": 171, "id": 172, "id": 173, "id": 174, "id": 175, "id": 176, "id": 177, "id": 178, "id": 179, "id": 180, "id": 181, "id": 182, "id": 183, "id": 184, "id": 185, "id": 186, "id": 187, "id": 188, "id": 189, "id": 190, "id": 191, "id": 192, "id": 193, "id": 194, "id": 195, "id": 196, "id": 197, "id": 198, "id": 199, "id": 200, "id": 201, "id": 202, "id": 203, "id": 204, "id": 205, "id": 206, "id": 207, "id": 208, "id": 209, "id": 210, "id": 211, "id": 212, "id": 213, "id": 214, "id": 215, "id": 216, "id": 217, "id": 218, "id": 219, "id": 220, "id": 221, "id": 222, "id": 223, "id": 224, "id": 225, "id": 226, "id": 227, "id": 228, "id": 229, "id": 230, "id": 231, "id": 232, "id": 233, "id": 234, "id": 235, "id": 236, "id": 237, "id": 238, "id": 239, "id": 240, "id": 241, "id": 242, "id": 243, "id": 244, "id": 245, "id": 246, "id": 247, "id": 248, "id": 249, "id": 250, "id": 251, "id": 252, "id": 253, "id": 254, "id": 255, "id": 256, "id": 257, "id": 258, "id": 259, "id": 260, "id": 261, "id": 262, "id": 263, "id": 264, "id": 265, "id": 266, "id": 267, "id": 268, "id": 269, "id": 270, "id": 271, "id": 272, "id": 273, "id": 274, "id": 275, "id": 276, "id": 277, "id": 278, "id": 279, "id": 280, "id": 281, "id": 282, "id": 283, "id": 284, "id": 285, "id": 286, "id": 287, "id": 288, "id": 289, "id": 290, "id": 291, "id": 292, "id": 293, "id": 294, "id": 295, "id": 296, "id": 297, "id": 298, "id": 299, "id": 300, "id": 301, "id": 302, "id": 303, "id": 304, "id": 305, "id": 306, "id": 307, "id": 308, "id": 309, "id": 310, "id": 311, "id": 312, "id": 313, "id": 314, "id": 315, "id": 316, "id": 317, "id": 318, "id": 319, "id": 320, "id": 321, "id": 322, "id": 323, "id": 324, "id": 325, "id": 326, "id": 327, "id": 328, "id": 329, "id": 330, "id": 331, "id": 332, "id": 333, "id": 334, "id": 335, "id": 336, "id": 337, "id": 338, "id": 339, "id": 340, "id": 341, "id": 342, "id": 343, "id": 344, "id": 345, "id": 346, "id": 347, "id": 348, "id": 349, "id": 350, "id": 351, "id": 352, "id": 353, "id": 354, "id": 355, "id": 356, "id": 357, "id": 358, "id": 359, "id": 360, "id": 361, "id": 362, "id": 363, "id": 364, "id": 365, "id": 366, "id": 367, "id": 368, "id": 369, "id": 370, "id": 371, "id": 372, "id": 373, "id": 374, "id": 375, "id": 376, "id": 377, "id": 378, "id": 379, "id": 380, "id": 381, "id": 382, "id": 383, "id": 384, "id": 385, "id": 386, "id": 387, "id": 388, "id": 389, "id": 390, "id": 391, "id": 392, "id": 393, "id": 394, "id": 395, "id": 396, "id": 397, "id": 398, "id": 399, "id": 400, "id": 401, "id": 402, "id": 403, "id": 404, "id": 405, "id": 406, "id": 407, "id": 408, "id": 409, "id": 410, "id": 411, "id": 412, "id": 413, "id": 414, "id": 415, "id": 416, "id": 417, "id": 418, "id": 419, "id": 420, "id": 421, "id": 422, "id": 423, "id": 424, "id": 425, "id": 426, "id": 427, "id": 428, "id": 429, "id": 430, "id": 431, "id": 432, "id": 433, "id": 434, "id": 435, "id": 436, "id": 437, "id": 438, "id": 439, "id": 440, "id": 441, "id": 442, "id": 443, "id": 444, "id": 445, "id": 446, "id": 447, "id": 448, "id": 449, "id": 450, "id": 451, "id": 452, "id": 453, "id": 454, "id": 455, "id": 456, "id": 457, "id": 458, "id": 459, "id": 460, "id": 461, "id": 462, "id": 463, "id": 464, "id": 465, "id": 466, "id": 467, "id": 468, "id": 469, "id": 470, "id": 471, "id": 472, "id": 473, "id": 474, "id": 475, "id": 476, "id": 477, "id": 478, "id": 479, "id": 480, "id": 481, "id": 482, "id": 483, "id": 484, "id": 485, "id": 486, "id": 487, "id": 488, "id": 489, "id": 490, "id": 491, "id": 492, "id": 493, "id": 494, "id": 495, "id": 496, "id": 497, "id": 498, "id": 499, "id": 500, "id": 501, "id": 502, "id": 503, "id": 504, "id": 505, "id": 506, "id": 507, "id": 508, "id": 509, "id": 510, "id": 511, "id": 512, "id": 513, "id": 514, "id": 515, "id": 516, "id": 517, "id": 518, "id": 519, "id": 520, "id": 521, "id": 522, "id": 523, "id": 524, "id": 525, "id": 526, "id": 527, "id": 528, "id": 529, "id": 530, "id": 531, "id": 532, "id": 533, "id": 534, "id": 535, "id": 536, "id": 537, "id": 538, "id": 539, "id": 540, "id": 541, "id": 542, "id": 543, "id": 544, "id": 545, "id": 546, "id": 547, "id": 548, "id": 549, "id": 550, "id": 551, "id": 552, "id": 553, "id": 554, "id": 555, "id": 556, "id": 557, "id": 558, "id": 559, "id": 560, "id": 561, "id": 562, "id": 563, "id": 564, "id": 565, "id": 566, "id": 567, "id": 568, "id": 569, "id": 570, "id": 571, "id": 572, "id": 573, "id": 574, "id": 575, "id": 576, "id": 577, "id": 578, "id": 579, "id": 580, "id": 581, "id": 582, "id": 583, "id": 584, "id": 585, "id": 586, "id": 587, "id": 588, "id": 589, "id": 590, "id": 591, "id": 592, "id": 593, "id": 594, "id": 595, "id": 596, "id": 597, "id": 598, "id": 599, "id": 600, "id": 601, "id": 602, "id": 603, "id": 604, "id": 605, "id": 606, "id": 607, "id": 608, "id": 609, "id": 610, "id": 611, "id": 612, "id": 613, "id": 614, "id": 615, "id": 616, "id": 617, "id": 618, "id": 619, "id": 620, "id": 621, "id": 622, "id": 623, "id": 624, "id": 625, "id": 626, "id": 627, "id": 628, "id": 629, "id": 630, "id": 631, "id": 632, "id": 633, "id": 634, "id": 635, "id": 636, "id": 637, "id": 638, "id": 639, "id": 640, "id": 641, "id": 642, "id": 643, "id": 644, "id": 645, "id": 646, "id": 647, "id": 648, "id": 649, "id": 650, "id": 651, "id": 652, "id": 653, "id": 654, "id": 655, "id": 656, "id": 657, "id": 658, "id": 659, "id": 660, "id": 661, "id": 662, "id": 663, "id": 664, "id": 665, "id": 666, "id": 667, "id": 668, "id": 669, "id": 670, "id": 671, "id": 672, "id": 673, "id": 674, "id": 675, "id": 676, "id": 677, "id": 678, "id": 679, "id": 680, "id": 681, "id": 682, "id": 683, "id": 684, "id": 685, "id": 686, "id": 687, "id": 688, "id": 689, "id": 690, "id": 691, "id": 692, "id": 693, "id": 694, "id": 695, "id": 696, "id": 697, "id": 698, "id": 699, "id": 700, "id": 701, "id": 702, "id": 703, "id": 704, "id": 705, "id": 706, "id": 707, "id": 708, "id": 709, "id": 710, "id": 711, "id": 712, "id": 713, "id": 714, "id": 715, "id": 716, "id": 717, "id": 718, "id": 719, "id": 720, "id": 721, "id": 722, "id": 723, "id": 724, "id": 725, "id": 726, "id": 727, "id": 728, "id": 729, "id": 730, "id": 731, "id": 732, "id": 733, "id": 734, "id": 735, "id": 736, "id": 737, "id": 738, "id": 739, "id": 740, "id": 741, "id": 742, "id": 743, "id": 744, "id": 745, "id": 746, "id": 747, "id": 748, "id": 749, "id": 750, "id": 751, "id": 752, "id": 753, "id": 754, "id": 755, "id": 756, "id": 757, "id": 758, "id": 759, "id": 760, "id": 761, "id": 762, "id": 763, "id": 764, "id": 765, "id": 766, "id": 767, "id": 768, "id": 769, "id": 770, "id": 771, "id": 772, "id": 773, "id": 774, "id": 775, "id": 776, "id": 777, "id": 778, "id": 779, "id": 780, "id": 781, "id": 782, "id": 783, "id": 784, "id": 785, "id": 786, "id": 787, "id": 788, "id": 789, "id": 790, "id": 791, "id": 792, "id": 793, "id": 794, "id": 795, "id": 796, "id": 797, "id": 798, "id": 799, "id": 800, "id": 801, "id": 802, "id": 803, "id": 804, "id": 805, "id": 806, "id": 807, "id": 808, "id": 809, "id": 810, "id": 811, "id": 812, "id": 813, "id": 814, "id": 815, "id": 816, "id": 817, "id": 818, "id": 819, "id": 820, "id": 821, "id": 822, "id": 823, "id": 824, "id": 825, "id": 826, "id": 827, "id": 828, "id": 829, "id": 830, "id": 831, "id": 832, "id": 833, "id": 834, "id": 835, "id": 836, "id": 837, "id": 838, "id": 839, "id": 840, "id": 841, "id": 842, "id": 843, "id": 844, "id": 845, "id": 446, "id": 447, "id": 448, "id": 449, "id": 450, "id": 451, "id": 452, "id": 453, "id": 454, "id": 455, "id": 456, "id": 457, "id": 458, "id": 459, "id": 460, "id": 461, "id": 462, "id": 463, "id": 464, "id": 465, "id": 466, "id": 467, "id": 468, "id": 469, "id": 470, "id": 471, "id": 472, "id": 473, "id": 474, "id": 475, "id": 476, "id": 477, "id": 478, "id": 479, "id": 480, "id": 481, "id": 482, "id": 483, "id": 484, "id": 485, "id": 486, "id": 487, "id": 488, "id": 489, "id": 490, "id": 491, "id": 492, "id": 493, "id": 494, "id": 495, "id": 496, "id": 497, "id": 498, "id": 499, "id": 500, "id": 501, "id": 502, "id": 503, "id": 504, "id": 505, "id": 506, "id": 507, "id": 508, "id": 509, "id": 510, "id": 511, "id": 512, "id": 513, "id": 514, "id": 515, "id": 516, "id": 517, "id": 518, "id": 519, "id": 520, "id": 521, "id": 522, "id": 523, "id": 524, "id": 525, "id": 526, "id": 527, "id": 528, "id": 529, "id": 530, "id": 531, "id": 532, "id": 533, "id": 534, "id": 535, "id": 536, "id": 537, "id": 538, "id": 539, "id": 540, "id": 541, "id": 542, "id": 543, "id": 544, "id": 545, "id": 546, "id": 547, "id": 548, "id": 549, "id": 550, "id": 551, "id": 552, "id": 553, "id": 554, "id": 555, "id": 556, "id": 557, "id": 558, "id": 559, "id": 560, "id": 561, "id": 562, "id": 563, "id": 564, "id": 565, "id": 566, "id": 567, "id": 568, "id": 569, "id": 570, "id": 571, "id": 572, "id": 573, "id": 574, "id": 575, "id": 576, "id": 577, "id": 578, "id": 579, "id": 580, "id": 581, "id": 582, "id": 583, "id": 584, "id": 585, "id": 586, "id": 587, "id": 588, "id": 589, "id": 590, "id": 591, "id": 592, "id":Errors (stderr)
no code extracted from response