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.parametrizeto 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.23and87032300formats 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
loggingwith the%(asctime)s | %(levelname)s | %(name)s | %(message)sformat) 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
)
Verification steps
Run the suite and these targeted checks before relying on the engine for a preference claim:
- Green matrix. Execute
pytest -vand confirm all parametrized cases pass. A red case here is a rule-logic regression, not a test flake — read thereasonfield, which names why the good failed. - Shift-level sensitivity. Re-run one qualifying case after changing the rule’s
levelfromHEADINGtoSUBHEADING. A material that shifted at heading level may share the good’s subheading and now fail; confirm the engine flips tono_shift, proving the level actually drives the comparison. - Exception prefix breadth. Add a material coded
8409910000and another coded8409999999under anexcepted_from={"8409"}rule. Both must fail asexcepted_material, confirming the exception matches by prefix and covers every subheading beneath the excepted heading. - Originating bypass. Assert that flipping a would-be offending material to
originating=Truechanges the outcome to qualifying. Tariff shift never applies to originating inputs, and a suite that does not cover this misses a common false-negative. - Fail-fast offender naming. For a multi-material good with exactly one non-shifting material, assert
offending_materialnames that part. Brokers need the specific SKU, not just a boolean, to remediate a claim. - Short-code guard. Confirm
test_short_code_raisespasses — 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_fromset 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.
Related
- Calculating regional value content with BOM hierarchies
- Implementing rule of origin checks in Python
- Rule of Origin Logic Engines
- Duty Formula Calculation Frameworks
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).