Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

MLOps Engineer Top 100 Interview Questions

100 MLOps engineer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable Python. Covers reproducible pipelines, data and feature validation, training infrastructure, model release and serving, monitoring, governance, cost and language-model operations, from the basics to production incidents.

100 questions

Questions 21-40 of 100

  1. Q21.How do you schedule hyperparameter search trials so they share hardware efficiently?

    Run trials as independent jobs from a central search controller, stop unpromising trials early and cap the total budget, so a search does not occupy a shared cluster for days on configurations that were never going to win.

    Making searches efficient:

    • Early stopping of trials: compare each trial's validation metric at checkpoints with others at the same point, and stop the bottom performers; most bad configurations reveal themselves early.
    • Adaptive search: choose new configurations based on earlier results instead of a fixed grid.
    • Parallelism with limits: run several trials at once, but cap concurrency so other teams keep access.
    • Small proxies first: search on a data subset or a smaller model, then confirm the top few at full scale.

    Operational practices:

    1. Log each trial as a tracked run, linked to the parent search.
    2. Set a budget in accelerator-hours, not just a trial count.
    3. Use interruptible capacity, since trials are independent and easy to restart.
    4. Reuse data preparation across trials through pipeline caching.

    Record the full search, including stopped trials, so the chosen configuration's selection process can be audited.

  2. Q22.Why must accelerator driver and runtime library versions match between training and serving images?

    Accelerator software comes in layers: a kernel driver on the host, runtime and math libraries inside the container, and the machine learning framework built against particular runtime versions. If those layers do not match, a model can fail to load, fall back silently to the slower general-purpose processor, or produce slightly different numbers.

    Common failure modes:

    • A newer runtime library inside the image needs a newer host driver than the nodes have, so the container starts but cannot use the accelerator.
    • A serving image built with a different framework version cannot read the saved model, or reads it with different default behavior.
    • Optimized kernels differ between library versions, changing numerical results enough to move borderline predictions.

    How to manage it:

    1. Maintain a small set of approved base images with tested combinations of runtime and framework versions.
    2. Build training and serving images from the same base, or record and test any difference.
    3. Track host driver versions in node pools, and roll out driver upgrades with compatibility tests.
    4. Add a startup check that confirms the accelerator is visible and fails the deployment if it is not.
  3. Q23.How do you keep Python dependencies consistent between training and serving?

    A model trained with one set of library versions can behave differently, or fail to load, under another. Lock the exact versions once and build both environments from that lock.

    1. One lock file for the libraries that touch the model: the framework, feature code and serialization libraries.
    2. Shared base image: build training and serving images from the same base and the same lock, adding only what each needs, such as a web server for serving.
    3. Record versions with the model: store the lock file or image digest in the registry entry.
    4. Test loading in the serving image: a pipeline step loads the trained model in the actual serving image and checks predictions against a small reference set.
    5. Upgrade deliberately: treat library upgrades as changes that require retraining or parity checks, not as routine updates.

    Common traps:

    • Unpinned transitive dependencies that change on rebuild.
    • Feature code duplicated in serving with small differences.
    • Serialization formats tied to library internals, which break when the library changes; prefer formats designed for stable model exchange.
  4. Q24.How do you store and move large model artifacts efficiently?

    Keep artifacts in durable object storage, referenced from the registry by immutable identifiers, and design deployments so servers load them quickly without downloading the same bytes repeatedly.

    Storage practices:

    • Content addressing: name artifacts by their checksum, so identical files are stored once and corruption is detectable.
    • Immutability: never overwrite a version; publish a new one.
    • Lifecycle rules: move old, unused versions to cheaper storage, and delete experiment artifacts that were never promoted, after a retention period.

    Loading at deployment:

    1. Cache on nodes: keep recently used artifacts on local disks, so new replicas on the same node start fast.
    2. Separate model from image: downloading weights at startup from a cache keeps container images small, while baking weights into the image simplifies versioning; choose based on artifact size and update frequency.
    3. Parallel and chunked downloads for multi-gigabyte files.
    4. Memory-mapped formats let servers load only the needed parts, and share memory between processes.

    Verify the checksum after every transfer, since a truncated download can produce a model that loads but behaves wrongly.

  5. Q25.How do you make sure the model file you deploy is the one that was trained and approved?

    Record a cryptographic checksum of the artifact when it is approved, and verify it at every later step, especially just before serving. Optionally sign the checksum, so you can also prove who approved it.

    Why it matters:

    • Files can be corrupted during transfer or overwritten by mistake.
    • Attackers who reach artifact storage can replace a model with one that behaves maliciously.
    • Some model file formats can execute code when loaded, so loading an untrusted file is a security risk, not just a quality one.

    Practices:

    1. Compute a SHA-256 checksum in the training pipeline, and store it in the registry with the version.
    2. Verify the checksum in the deployment pipeline and again when the server loads the model.
    3. Restrict write access to artifact storage to the pipeline's identity.
    4. Prefer serialization formats that store only weights and structure, not executable code.
    5. Sign approved artifacts, and have servers reject unsigned or mismatched files.

    Example

    The checksum recorded at approval detects a changed file.

    import hashlib
    import tempfile
    from pathlib import Path
    
    
    def sha256_of(path, chunk_size=1 << 20):
        digest = hashlib.sha256()
        with open(path, "rb") as f:
            while chunk := f.read(chunk_size):
                digest.update(chunk)
        return digest.hexdigest()
    
    
    with tempfile.TemporaryDirectory() as tmp:
        model = Path(tmp) / "model.bin"
        model.write_bytes(b"weights for version 7")
        approved = sha256_of(model)
    
        print(sha256_of(model) == approved)  # True
        model.write_bytes(b"weights for version 7, modified")
        print(sha256_of(model) == approved)  # False

    Reading in chunks keeps memory use small even for multi-gigabyte files.

  6. Q26.How do you design a quality gate that compares a candidate model with the production model?

    Evaluate both models on the same recent, held-out dataset, then promote the candidate only if its primary metric improves by a meaningful margin and no guardrail metric gets worse beyond an agreed tolerance. Automating the rule removes debate from each release and stops silent regressions.

    Elements of a good gate:

    1. Same evaluation data: both models scored on an identical, frozen dataset that neither was trained on.
    2. Primary metric with a minimum gain: a candidate that is only equal brings risk without benefit.
    3. Guardrails with tolerances: critical slices, such as new users or a key region, plus latency and model size.
    4. Statistical care: small differences on small evaluation sets are noise; use confidence intervals or a minimum sample per slice.
    5. Clear output: a decision with reasons, stored with the candidate's registry entry.

    When the gate fails: notify the owners with the reasons, keep the candidate for analysis and leave production unchanged. Allow a documented manual override for cases such as a required data change, with an approver recorded.

    Example

    The candidate improves the primary metric but loses recall on new users, so it is rejected.

    GUARDRAILS = {
        "recall_new_users": ("higher", 0.02),
        "p95_latency_ms": ("lower", 5),
    }
    
    
    def gate(candidate, production, primary="auc", min_gain=0.002):
        failures = []
        gain = candidate[primary] - production[primary]
        if gain < min_gain:
            failures.append(f"{primary} gain {gain:+.3f} is below {min_gain}")
        for metric, (better, tolerance) in GUARDRAILS.items():
            change = candidate[metric] - production[metric]
            worse = -change if better == "higher" else change
            if worse > tolerance:
                failures.append(f"{metric} worse by {worse:.3g}")
        return ("promote" if not failures else "reject"), failures
    
    
    production = {"auc": 0.842, "recall_new_users": 0.61, "p95_latency_ms": 38}
    candidate = {"auc": 0.851, "recall_new_users": 0.57, "p95_latency_ms": 41}
    
    decision, reasons = gate(candidate, production)
    print(decision)  # reject
    print(reasons)  # ['recall_new_users worse by 0.04']
  7. Q27.What are behavioral tests for a model, and how do you write them?

    Behavioral tests check that a model responds sensibly to controlled changes in its input, like unit tests for behavior rather than for code. Aggregate metrics can improve while a model learns a dangerous shortcut, and these tests catch that.

    Three common kinds:

    • Invariance tests: changing something that should not matter leaves the prediction unchanged, such as the display language of the app or the formatting of an address.
    • Directional tests: changing something in a known direction moves the prediction the expected way, such as a larger, riskier transaction never lowering the fraud score.
    • Minimum functionality tests: obvious cases get obvious answers, such as a known fraud pattern scoring high.

    How to run them:

    1. Write the cases with domain experts, starting from past incidents and known risks.
    2. Generate variations automatically from real examples, such as copying a transaction and changing one field.
    3. Run them in the pipeline as a gate, with a failure threshold for each test family.
    4. Add a new test whenever a production problem reveals a missing behavior.

    Example

    A small scoring function stands in for a model; the tests check invariance, direction and an obvious case.

    def fraud_score(txn):
        score = 0.1 + min(txn["amount"] / 100_000, 0.5)
        if txn["country"] != txn["card_country"]:
            score += 0.3
        return round(min(score, 1.0), 3)
    
    
    base = {"amount": 20_000, "country": "IN", "card_country": "IN", "app_language": "en"}
    
    print(fraud_score(base) == fraud_score({**base, "app_language": "hi"}))  # True
    print(fraud_score({**base, "amount": 60_000}) >= fraud_score(base))  # True
    print(fraud_score({**base, "amount": 60_000, "country": "SG"}) > 0.8)  # True
  8. Q28.How do you check model performance on critical data slices before release?

    Define the slices that matter before evaluating, compute metrics for each one on held-out data and compare them with the production model. An overall improvement can hide a serious regression for a group that is small in the data but important to the business or to fairness.

    Choosing slices:

    • Business-critical groups: high-value customers, new markets or key product categories.
    • Known weak spots: segments that caused past incidents.
    • Fairness-relevant groups, where the law or company policy requires equal treatment.
    • Data conditions: missing features, new users with little history and rare categories.

    Making slice results trustworthy:

    1. Require a minimum number of examples per slice, and report confidence intervals, since small slices are noisy.
    2. Compare each slice with the production model's result on the same data, not with the overall average.
    3. Set tolerances per slice, stricter for the most important ones.
    4. Keep the slice definitions in code, so every candidate is measured the same way.

    Also look for slices automatically, by searching for groups where errors are much higher than average, and turn real findings into permanent slices.

  9. Q29.What does a model promotion workflow look like, from candidate to production?

    A promotion workflow moves each model version through defined stages, with automated checks and recorded approvals at each transition. It makes the path to production predictable and auditable.

    stateDiagram-v2
      state "Candidate" as C
      state "Staging" as S
      state "Production" as P
      state "Archived" as A
      [*] --> C
      C --> S: Quality gate passes
      C --> A: Gate fails
      S --> P: Shadow and canary checks pass, approval recorded
      S --> A: Checks fail
      P --> A: Replaced by a newer version

    What happens at each stage:

    • Candidate: registered automatically by the training pipeline with lineage and metrics.
    • Staging: deployed to a pre-production environment or in shadow mode, receiving real traffic copies without affecting users.
    • Production: serving live traffic, often reached through a canary.
    • Archived: kept for audit and rollback, but not served.

    Rules that make it work:

    1. Only the pipeline can create candidates; people cannot upload files directly to production.
    2. Each transition records who or what approved it and why.
    3. The previous production version stays ready for immediate rollback.
    4. Higher-risk models require a human approval step in addition to automated checks.
  10. Q30.How does shadow deployment work for a model, and what does it tell you?

    In shadow mode, the new model receives a copy of real production requests and makes predictions, but its outputs are only logged, never returned to users. It tests the model under real traffic and real data with no user impact.

    sequenceDiagram
      participant C as Client
      participant S as Serving layer
      participant P as Production model
      participant N as Shadow model
      participant L as Comparison log
      C->>S: Prediction request
      S->>P: Request
      P-->>S: Prediction
      S-->>C: Production prediction
      S->>N: Copy of request, asynchronously
      N-->>L: Shadow prediction
      S->>L: Production prediction

    What shadow mode reveals:

    • Whether the new model handles real inputs without errors, including rare and malformed ones.
    • Latency and resource use under real load.
    • How often its decisions differ from production, and on which kinds of requests.

    What it cannot reveal: the effect on user behavior and business outcomes, since users never see shadow predictions. That needs a canary or an experiment.

    Practical points: send shadow traffic asynchronously, so shadow failures never slow production, and make sure shadow predictions cannot trigger side effects such as emails or payments.

  11. Q31.How do you compare a shadow model's predictions with the production model's?

    Join the two models' outputs by request ID, then measure how often their decisions agree, how far their scores differ, and where the disagreements concentrate. The aim is to understand every difference before real users see it.

    Useful comparisons:

    1. Decision agreement: the share of requests where both models make the same decision at the business threshold.
    2. Score differences: mean and percentiles of the absolute difference.
    3. Flip analysis: which requests changed decision, broken down by segment.
    4. Distribution comparison: the share of predictions above each threshold for both models.
    5. Operational metrics: latency and error rates of both.

    Interpreting the results:

    • Very high agreement means low risk, but also little benefit; a better model must disagree somewhere.
    • Disagreements should concentrate where the new model was meant to improve; review samples of flips with domain experts.
    • Large changes in the share of positive decisions affect downstream teams, such as reviewers handling flagged cases, so warn them before launch.

    Example

    Eight requests are compared at a threshold of 0.5; two decisions flip.

    import statistics
    
    production = [0.91, 0.12, 0.55, 0.30, 0.78, 0.05, 0.62, 0.48]
    shadow = [0.88, 0.10, 0.45, 0.35, 0.81, 0.07, 0.71, 0.52]
    threshold = 0.5
    
    pairs = list(zip(production, shadow))
    flips = [i for i, (p, s) in enumerate(pairs) if (p >= threshold) != (s >= threshold)]
    mean_gap = statistics.mean(abs(p - s) for p, s in pairs)
    
    print(f"agreement: {len(pairs) - len(flips)}/{len(pairs)}")  # agreement: 6/8
    print(f"mean absolute difference: {mean_gap:.3f}")  # mean absolute difference: 0.048
    print("flipped requests:", flips)  # flipped requests: [2, 7]
  12. Q32.How does a canary release for a model differ from a canary for ordinary code, and which signals decide whether it continues?

    Route a small share of live traffic to the new model, compare its signals with the current model's over a defined period, and increase the share in steps only while every signal stays within its limits. Unlike shadow mode, the canary affects real users, so it measures real outcomes.

    flowchart LR
      A["1% of traffic"] --> B{"Signals within limits?"}
      B -->|Yes| C["10% of traffic"]
      C --> D{"Signals within limits?"}
      D -->|Yes| E["50%, then 100%"]
      B -->|No| F["Automatic rollback"]
      D -->|No| F

    Signals to watch:

    • Service health: error rate, latency percentiles and resource use.
    • Prediction behavior: score distribution and the share of positive decisions.
    • Business outcomes: conversion, complaints or manual review rates, where they arrive quickly enough.

    Making it reliable:

    1. Assign users consistently, so a user does not switch between models within a session.
    2. Define rollback limits before starting, and automate the rollback.
    3. Hold each step long enough to see daily patterns; outcomes that arrive later need a longer canary or a follow-up experiment.
    4. Compare with the stable model during the same period, not with last week.
  13. Q33.How do you route a fixed share of traffic to a new model consistently for each user?

    Hash the user's identifier with a salt specific to the rollout, map the hash to a bucket from 0 to 9,999, and send buckets below the rollout percentage to the new model. The same user always lands in the same bucket, so their experience stays consistent between requests.

    Why hashing instead of random choice per request:

    • Users see consistent behavior, which matters for anything they notice, such as recommendations.
    • Outcome metrics are cleaner, because each user belongs to one group.
    • No routing table needs storing or synchronizing across servers.

    Ramping up safely: raising the percentage only adds new buckets, so users already on the new model stay there; nobody switches back and forth.

    Details to get right:

    • Use a new salt for each independent rollout, so the same users are not always the first to get every change.
    • Exclude internal test accounts or route them explicitly.
    • Log the bucket and model version with each prediction, so analysis can attribute outcomes correctly.

    Example

    At 5% about one user in twenty gets the canary, and raising it to 20% keeps all of them there.

    import hashlib
    
    
    def route(user_id, canary_pct, salt="ranker-v9"):
        digest = hashlib.sha256(f"{salt}:{user_id}".encode()).hexdigest()
        bucket = int(digest[:8], 16) % 10_000
        return "canary" if bucket < canary_pct * 100 else "stable"
    
    
    users = [f"user-{i}" for i in range(20_000)]
    at_5 = {u: route(u, 5) for u in users}
    at_20 = {u: route(u, 20) for u in users}
    
    print(sum(v == "canary" for v in at_5.values()))  # 999
    print(sum(v == "canary" for v in at_20.values()))  # 4047
    print(all(at_20[u] == "canary" for u in users if at_5[u] == "canary"))  # True
  14. Q34.What does a model server do beyond calling the model's predict function?

    A production model server turns a model file into a reliable network service. Most of its work is around the model: loading, validating, batching, observing and protecting it.

    1. Loading and versioning: loads the approved artifact, verifies its checksum and reports which version it serves.
    2. Warm-up: runs sample requests at startup, so the first real users do not pay compilation or caching costs.
    3. Input validation: rejects malformed requests before they reach the model.
    4. Preprocessing and postprocessing: applies the same feature transformations used in training, and turns raw outputs into business responses.
    5. Batching: groups concurrent requests to use hardware efficiently.
    6. Health checks: separate signals for "process is alive" and "model is loaded and ready".
    7. Observability: latency, errors, prediction distributions and model version on every log line.
    8. Protection: timeouts, request size limits, concurrency limits and graceful shutdown that finishes in-flight requests.

    Teams often use a shared serving framework for this, so each model team supplies only the model and its transformations.

  15. Q35.How does dynamic request batching trade latency for throughput?

    The server waits briefly to collect several requests, then runs them through the model together. Accelerators process a batch of many inputs in little more time than a single input, so throughput rises sharply, while each request may wait a few extra milliseconds.

    The two settings:

    • Maximum batch size: limited by memory and by how long a large batch takes.
    • Maximum wait: the longest a request waits for others; this caps the added latency.

    How to tune them:

    1. Measure model time for batch sizes 1, 2, 4 and upwards on the real hardware.
    2. Set the maximum wait from the latency budget, such as 5 to 10 milliseconds.
    3. Load test with realistic traffic, since batches only fill when requests arrive often enough.
    4. Watch the batch size distribution in production: mostly single-request batches mean the wait is too short or traffic too low to benefit.

    When batching does not help: at low traffic, batches rarely fill and requests only wait; and for models whose time grows linearly with batch size, such as many small models on general-purpose processors, the gain is small.

    Example

    A model takes 20 ms for one request and 40 ms for a batch of 16, with a maximum wait of 10 ms.

    1. Without batching, one replica handles 1 / 0.020 = 50 requests per second.
    2. With full batches: 16 / 0.040 = 400 requests per second, eight times more.
    3. Worst-case latency rises from 20 ms to 10 + 40 = 50 ms.
    4. If the latency target is 100 ms, batching cuts the replicas needed for 2,000 requests per second from 40 to 5.
  16. Q36.How do you set a latency budget for a model endpoint?

    Start from the end-to-end target the product needs, then allocate it across every stage of the request, keeping headroom for spikes. A budget shows which stage to optimize and stops one team from consuming everyone's margin.

    Typical stages:

    • Network and gateway overhead.
    • Request validation and preprocessing.
    • Feature retrieval from online stores or other services.
    • Model inference, including any batching wait.
    • Postprocessing, business rules and response serialization.

    How to build it:

    1. Measure each stage's latency distribution under realistic load, not just its average.
    2. Allocate each stage a percentile target, such as its 99th percentile, and keep 10% to 20% of the total as headroom.
    3. Run independent calls in parallel, such as fetching features from two stores at once.
    4. Set timeouts per stage from the budget, with a fallback, such as a default feature value or a simpler model, when a stage runs out of time.

    Adding stages' 99th percentiles gives a conservative total, since all stages rarely hit their slowest at once; measure the real end-to-end percentile as the final check.

    Example

    A checkout fraud check must answer within 150 ms at the 99th percentile.

    1. Gateway and network: 20 ms.
    2. Feature retrieval, two lookups in parallel: 40 ms.
    3. Model inference, including batching wait: 60 ms.
    4. Business rules and response: 10 ms.
    5. Total: 20 + 40 + 60 + 10 = 130 ms, leaving 20 ms of headroom.
    6. Feature retrieval gets a 45 ms timeout with default values, so a slow store degrades accuracy slightly instead of failing checkout.
  17. Q37.How do you reduce model loading and cold-start time when scaling out?

    When traffic rises, new replicas are useless until they have downloaded the model, loaded it into memory and warmed up. Long cold starts force teams to overprovision, so shortening them saves money and improves reliability.

    Where the time goes:

    • Pulling a large container image.
    • Downloading model weights.
    • Loading weights into memory and onto accelerators.
    • First-request costs, such as compiling optimized kernels or filling caches.

    Ways to reduce it:

    1. Smaller images: keep model weights out of the image, and use slim base images.
    2. Node-level caches: keep images and weights on local disks of nodes that serve the model.
    3. Faster formats: memory-mapped weight files load lazily and can be shared between processes.
    4. Warm-up requests: run representative requests before reporting ready, so users never hit the slow first calls.
    5. Readiness checks: route traffic only after the model is loaded and warmed.
    6. Predictive scaling: scale up ahead of known traffic peaks, and keep a minimum number of warm replicas.

    Measure startup time as a metric, broken down by phase, so regressions from larger models are noticed.

  18. Q38.How do you autoscale model servers, and which metric should drive scaling?

    Scale on the metric that most directly predicts latency for your workload, with limits and smoothing so the system does not oscillate. The usual general-purpose processor utilization metric is often a poor fit for model servers.

    Processor utilizationAccelerator utilizationQueue length or concurrencyRequest rate
    Works for models served on general-purpose processorsShows how busy accelerators areCounts requests waiting or in flight per replicaCounts incoming requests per second
    Misleading when the accelerator, not the processor, is the bottleneckCan read high even when batches are small and inefficientTracks latency closely, so it is often the best signalSimple and predictable when the cost per request is stable
    Use with care on accelerator nodesUse alongside a latency or queue signalRecommended default for most model serversGood for predictable traffic with stable request sizes

    Practical settings:

    1. Set a minimum replica count that covers baseline traffic and absorbs a sudden spike while new replicas start.
    2. Scale up quickly and down slowly, to avoid repeatedly removing replicas that are needed again minutes later.
    3. Account for cold-start time: if startup takes two minutes, scale on a target that leaves two minutes of headroom.
    4. Cap the maximum, so a traffic bug cannot exhaust the budget.
  19. Q39.How do you serve many models efficiently on shared infrastructure?

    When an organization has hundreds of models, giving each its own always-on replicas wastes money, since most receive little traffic. Shared serving packs models onto common servers and loads them on demand.

    Approaches:

    • Multi-model servers: one server process hosts many models, loading them into memory when requested and evicting the least recently used ones when memory runs low.
    • Tiered placement: high-traffic models get dedicated replicas; long-tail models share a pool.
    • Scale to zero: rarely used models have no replicas until a request arrives, accepting a slow first request.
    • Shared base models: many small task-specific adapters can share one large base model in memory.

    Risks to manage:

    1. Noisy neighbors: one model's traffic spike slows others; set per-model concurrency limits.
    2. Load latency: the first request to an evicted model is slow; keep important models pinned in memory.
    3. Dependency conflicts: models needing different library versions cannot share a process; standardize runtimes or group models by runtime.
    4. Blast radius: one bad model that crashes the process takes others down; isolate risky models.

    Track memory, load events and latency per model, so placement decisions are based on data.

  20. Q40.How do you validate prediction requests and responses at the API boundary?

    Check every request against an explicit schema before it reaches the model, and check responses before they leave. Models rarely fail loudly on bad input; they return confident, wrong answers instead.

    Request checks:

    • Required fields present, with correct types.
    • Values within valid ranges and categories from allowed sets.
    • Unexpected fields rejected or logged, which catches client bugs early.
    • Size limits on lists and text fields, which protect latency and memory.

    Response checks:

    • Scores within valid ranges and probabilities that sum correctly.
    • Required output fields present, and the model version included.
    • Fallback behavior defined if the model returns an invalid output.

    Operational practices:

    1. Return clear error messages that name the failing field, so client teams can fix problems quickly.
    2. Count validation failures per field and per client, and alert on spikes, which usually signal a broken upstream change.
    3. Version the schema, and keep it shared between client and server code.

    Example

    A small validator checks types, ranges, allowed values and unexpected fields.

    SCHEMA = {
        "customer_id": (str, None),
        "basket_value": (float, (0, 1_000_000)),
        "items": (int, (1, 500)),
        "channel": (str, {"app", "web"}),
    }
    
    
    def validate(payload):
        errors = []
        for field, (expected, rule) in SCHEMA.items():
            if field not in payload:
                errors.append(f"{field}: missing")
                continue
            value = payload[field]
            if expected is float and type(value) is int:
                value = float(value)
            if type(value) is not expected:
                errors.append(f"{field}: expected {expected.__name__}")
            elif isinstance(rule, tuple) and not rule[0] <= value <= rule[1]:
                errors.append(f"{field}: out of range")
            elif isinstance(rule, set) and value not in rule:
                errors.append(f"{field}: unknown value {value!r}")
        errors += [f"{name}: unexpected field" for name in sorted(set(payload) - set(SCHEMA))]
        return errors
    
    
    print(validate({"customer_id": "c-17", "basket_value": 1250, "items": 3, "channel": "app"}))
    print(validate({"customer_id": "c-18", "basket_value": -5.0, "items": "3", "channel": "kiosk", "debug": True}))
    # Output:
    # []
    # ['basket_value: out of range', 'items: expected int', "channel: unknown value 'kiosk'", 'debug: unexpected field']