Testing USMCA tariff-shift rules in Python

A change-in-tariff-classification rule qualifies a good as originating when every non-originating material is classified in a different part of the Harmonized System than the finished good — but “different” is defined at a specific level (chapter, heading, or subheading) that varies rule by rule, and the exceptions (“except from heading 84.09”) are where preference claims quietly go wrong. The only defensible way to ship a tariff-shift engine is to pin its behaviour with a parametrized test suite that exercises the shift level, the exceptions, and the boundary between a genuine transformation and simple assembly. This page builds a compact CTC engine and the pytest matrix that proves it, so the origin determination it feeds into your rule of origin checks is reproducible rather than a black box.

Prerequisites

This suite assumes a specific rules representation and toolchain. Confirm each before applying it:

  • Python 3.10+ and pytest. The tests lean on pytest.mark.parametrize to drive one assertion body across dozens of code pairs; without parametrization the matrix degrades into copy-pasted cases that drift out of sync with the rules table.
  • Digit-normalized HS codes. Every code — the finished good and each non-originating material — must be a digit-only string of consistent length before it reaches the engine. Mixed 8703.23 and 87032300 formats will silently defeat the level comparisons. Normalize on ingestion, the same way the HTS schedule database design stores digit-only keys.
  • A CTC rules table keyed by the good’s heading or subheading. Each rule states the required shift level and an optional set of excepted source classifications. This page ships a small in-memory table; in production it is the parsed output of the USMCA product-specific rules of origin, versioned alongside your tariff snapshots.
  • Materials pre-tagged originating or non-originating. Tariff-shift rules are evaluated only against non-originating materials — originating inputs never need to shift. That tagging is upstream work; an untagged material must be treated as non-originating, never skipped.
  • Structured logging configured (stdlib logging with the %(asctime)s | %(levelname)s | %(name)s | %(message)s format) so a failing shift is traceable to the exact offending material during a broker review.

Implementation

The engine reduces a tariff shift to three comparisons at increasing granularity — chapter (2 digits), heading (4 digits), subheading (6 digits) — plus an exception check. A material satisfies the rule when it is classified outside the good at the required level and is not one of the explicitly excepted classifications.

import logging
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional

logging.basicConfig(
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
    level=logging.INFO,
)
logger = logging.getLogger("ctc_engine")


class ShiftLevel(Enum):
    CHAPTER = 2       # change to chapter (CC)
    HEADING = 4       # change to heading (CTH)
    SUBHEADING = 6    # change to subheading (CTSH)


@dataclass(frozen=True)
class TariffShiftRule:
    good_prefix: str                       # heading/subheading the rule applies to
    level: ShiftLevel
    # Source classifications that CANNOT confer origin even if they shift, e.g.
    # "a change ... except from heading 84.09". Stored digit-only, any length.
    excepted_from: frozenset[str] = field(default_factory=frozenset)


@dataclass(frozen=True)
class Material:
    part_number: str
    hts_code: str
    originating: Optional[bool] = None     # None => treat as non-originating


@dataclass(frozen=True)
class ShiftResult:
    good_code: str
    qualifies: bool
    reason: str
    offending_material: Optional[str] = None


def _segment(code: str, level: ShiftLevel) -> str:
    """The chapter/heading/subheading prefix of a digit-only HS code."""
    if len(code) < level.value:
        raise ValueError(f"HS code {code!r} too short for {level.name} comparison")
    return code[: level.value]


def _is_excepted(material_code: str, excepted: frozenset[str]) -> bool:
    # An exception matches when the material's code starts with an excepted prefix,
    # so "8409" excepts 8409.91.0000 and every subheading beneath it.
    return any(material_code.startswith(x) for x in excepted)


def evaluate_tariff_shift(
    good_code: str,
    materials: list[Material],
    rule: TariffShiftRule,
) -> ShiftResult:
    """A good qualifies only if EVERY non-originating material shifts and none is excepted."""
    good_seg = _segment(good_code, rule.level)

    for m in materials:
        if m.originating is True:
            continue  # originating materials are never subject to the shift test

        material_seg = _segment(m.hts_code, rule.level)

        # Fail 1: an excepted source classification blocks the shift outright.
        if _is_excepted(m.hts_code, rule.excepted_from):
            logger.info(
                "Good %s FAILS: material %s (%s) is excepted from the shift.",
                good_code, m.part_number, m.hts_code,
            )
            return ShiftResult(good_code, False, "excepted_material", m.part_number)

        # Fail 2: material sits in the same segment as the good — no shift occurred.
        if material_seg == good_seg:
            logger.info(
                "Good %s FAILS: material %s (%s) does not shift at %s.",
                good_code, m.part_number, m.hts_code, rule.level.name,
            )
            return ShiftResult(good_code, False, "no_shift", m.part_number)

    logger.info("Good %s QUALIFIES under %s shift.", good_code, rule.level.name)
    return ShiftResult(good_code, True, "all_materials_shift")

The rule is conjunctive and fails fast: one non-originating material that shares the good’s chapter, heading, or subheading — or that matches an excepted prefix — sinks the entire claim and names the offender. That “every material must shift” semantics, plus the prefix-based exception match, are precisely what the test matrix below has to hold in place.

import pytest

# Compact USMCA-style CTC table. In production this is parsed and versioned.
RULE_8703 = TariffShiftRule(
    good_prefix="8703",
    level=ShiftLevel.HEADING,                  # CTH into 87.03
    excepted_from=frozenset({"8409"}),         # ... except from engine parts 84.09
)


@pytest.mark.parametrize(
    "good, mats, expect_qualify, expect_reason",
    [
        # All non-originating materials shift from a different heading -> qualifies.
        ("8703230000",
         [Material("seat", "9401610000", originating=False),
          Material("glass", "7007210000", originating=False)],
         True, "all_materials_shift"),
        # A material already in heading 8703 (a body shell) -> no shift.
        ("8703230000",
         [Material("body", "8703900000", originating=False)],
         False, "no_shift"),
        # A material from excepted heading 8409 -> blocked even though it shifts.
        ("8703230000",
         [Material("piston", "8409910000", originating=False)],
         False, "excepted_material"),
        # Same offending material but tagged originating -> exception does not apply.
        ("8703230000",
         [Material("piston", "8409910000", originating=True)],
         True, "all_materials_shift"),
        # Untagged material is treated as non-originating and still shifts -> qualifies.
        ("8703230000",
         [Material("harness", "8544300000", originating=None)],
         True, "all_materials_shift"),
    ],
)
def test_ctc_heading_shift(good, mats, expect_qualify, expect_reason):
    result = evaluate_tariff_shift(good, mats, RULE_8703)
    assert result.qualifies is expect_qualify
    assert result.reason == expect_reason


def test_short_code_raises():
    with pytest.raises(ValueError):
        evaluate_tariff_shift(
            "870", [Material("x", "9401610000", originating=False)], RULE_8703
        )
Decision flow and test matrix for a heading-level tariff shift A finished good in heading 8703 is tested against each non-originating material under a change-to-heading rule with an exception from heading 8409. For every material the engine asks three questions in order. First, is the material originating? If yes it is skipped. Second, does the material match an excepted prefix such as 8409? If yes the good fails with reason excepted material. Third, does the material share the good's four-digit heading? If yes the good fails with reason no shift. Only if every non-originating material clears all three checks does the good qualify. The table lists five parametrized cases: a seat and glass from other headings qualify; a body panel in 8703 fails as no shift; a piston in 8409 fails as excepted; the same piston tagged originating is skipped and qualifies; and an untagged harness is treated as non-originating, shifts, and qualifies. non-originating? excepted prefix? same heading? QUALIFIES FAIL: excepted FAIL: no shift Parametrized cases (good 8703) seat 9401 + glass 7007 (non-orig) qualifies body 8703 (non-orig) no shift piston 8409 (non-orig) excepted piston 8409 (originating) qualifies harness 8544 (untagged → non-orig) qualifies Rule: CTH into 87.03, except from heading 84.09 · every non-orig material must shift
Each non-originating material runs the three-question flow; the parametrized matrix pins one qualifying case, a same-heading failure, an exception failure, and the originating and untagged edge cases.

Verification steps

Run the suite and these targeted checks before relying on the engine for a preference claim:

  1. Green matrix. Execute pytest -v and confirm all parametrized cases pass. A red case here is a rule-logic regression, not a test flake — read the reason field, which names why the good failed.
  2. Shift-level sensitivity. Re-run one qualifying case after changing the rule’s level from HEADING to SUBHEADING. A material that shifted at heading level may share the good’s subheading and now fail; confirm the engine flips to no_shift, proving the level actually drives the comparison.
  3. Exception prefix breadth. Add a material coded 8409910000 and another coded 8409999999 under an excepted_from={"8409"} rule. Both must fail as excepted_material, confirming the exception matches by prefix and covers every subheading beneath the excepted heading.
  4. Originating bypass. Assert that flipping a would-be offending material to originating=True changes the outcome to qualifying. Tariff shift never applies to originating inputs, and a suite that does not cover this misses a common false-negative.
  5. Fail-fast offender naming. For a multi-material good with exactly one non-shifting material, assert offending_material names that part. Brokers need the specific SKU, not just a boolean, to remediate a claim.
  6. Short-code guard. Confirm test_short_code_raises passes — a code too short for the requested level must raise, not silently truncate, before the result ever reaches the duty formula calculation frameworks downstream.

Edge cases & gotchas

  • Tariff shift is often only half the rule. Many USMCA product-specific rules read “a change to heading X and a regional value content of not less than N percent.” This engine proves the shift; pair it with a regional value content calculation over the BOM and require both to pass. Testing the shift alone can green-light a good that fails its RVC leg.
  • Exception headings cut both ways. An “except from” clause can also be phrased as “provided there is also a change from” a specific subheading. Model exclusions and required co-shifts as separate fields; collapsing them into one excepted_from set will misclassify goods whose rule demands an additional shift rather than forbidding one.
  • Self-produced materials. A non-originating material may itself be an intermediate the producer made from originating inputs. Its classification for the shift test is the intermediate’s HS code, not its inputs’ — determine and tag it before the test, or the shift comparison runs against the wrong level.
  • Six-digit international versus ten-digit national codes. The HS is harmonized only to six digits; tariff-shift rules are written at chapter, heading, or subheading level, all within those six. Comparing at eight or ten digits imports national tariff detail the rule never intended and can manufacture spurious shifts. Segment strictly at 2, 4, or 6.
  • Empty non-originating set. A good built entirely from originating materials trivially “qualifies” because the loop finds nothing to test. That is correct, but make it explicit in a test so a future refactor does not turn the vacuous pass into a vacuous fail.
  • Rules-table versioning. Product-specific rules change with agreement updates and HS nomenclature revisions. Pin the rules table to the same version as the tariff snapshot that classified the materials, or a good can pass against a rule that did not exist on its entry date — the same temporal hazard the rule of origin logic engine guards elsewhere.

Up: Rule of Origin Logic Engines

Authoritative references: USMCA Chapter 4 and Annex 4-B (Product-Specific Rules of Origin); USMCA Uniform Regulations; 19 CFR Part 182; WCO HS 2022 Nomenclature; HTSUS General Notes (USITC).