Generating DEA Form 41 Destruction Records
Assemble a DEA Form 41 destruction record from witnessed-destruction ledger events, bind it to a snapshot hash, and attest the non-retrievable standard.
A DEA Form 41 is only as trustworthy as the events it summarizes. The form asks a registrant to certify an inventory of controlled substances destroyed — each item with its NDC, quantity, and form — along with the method, date, place, and the two witnesses who observed the destruction. If that certification is typed by hand from memory or from a mutable spreadsheet, it can drift from what actually happened; if it is assembled deterministically from witnessed-destruction events already committed to an append-only ledger, the form becomes a rendering of the record rather than a re-entry of it. This recipe assembles a Form 41 destruction record from ledger events and binds it to a snapshot hash so the certified figures cannot be altered after signing. It sits within Controlled Substance Waste & Destruction Logging, which defines the witnessed-waste and destruction events this recipe consumes.
The design target is a single function: take the witnessed-destruction events for a destruction batch, verify each carries two distinct witnesses and a non-retrievable method attestation, aggregate them into the Form 41 line items, and freeze the result under a snapshot hash. When that holds, the Form 41 an inspector receives is provably the same set of destructions the ledger recorded.
Problem framing
The parent topic establishes that destruction is a witnessed disposition under 21 CFR Part 1317 and that DEA Form 41 is the registrant record of what was destroyed. What it leaves open is the mechanical question this page answers: given a set of witnessed-destruction events in the ledger, how do you produce a Form 41 whose figures are guaranteed to match those events and cannot be edited afterward? The problem has three parts — select the events belonging to one destruction batch, verify each meets the Form 41 evidentiary bar (two distinct witnesses, a non-retrievable method, a quantity with explicit units), and freeze the aggregate under a hash so the signed form is bound to an exact ledger state. A reconstruction of ledger state at an arbitrary past instant, which the same snapshot technique enables, is treated in Point-in-Time Inventory Reconstruction.
Prerequisites & environment
- Python 3.11+ — uses
dataclasses(frozen=True),enum.Enum,Decimal, and union-type syntax. - Standard library only:
dataclasses,enum,hashlib,json,decimal,logging,datetime. Addpytestfor the test block. - Regulatory context you should already hold:
21 CFR Part 1317(controlled-substance disposal and the non-retrievable standard), the DEA Form 41 fields (inventory of substances destroyed, method, date, place, and two witnessing signatures), and45 CFR § 164.312(b)(audit controls). The witnessed-destruction event shape comes from the parent topic. - You are consuming events, not creating destructions: this recipe assumes witnessed-destruction events already exist in the append-only ledger and assembles a certifiable record from them.
Implementation: assembling a snapshot-bound Form 41
The assembler takes the witnessed-destruction events for a batch, rejects any that fail the evidentiary checks, groups the survivors into line items by NDC and dosage form, and binds the whole record to a SHA-256 snapshot hash computed over the sorted, canonicalized content. The snapshot hash is what makes the certified form tamper-evident: re-hashing the stored record must reproduce it, or the record was altered after assembly.
from __future__ import annotations
import hashlib
import json
import logging
from collections import defaultdict
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum
logger = logging.getLogger("cs_ledger.form41")
class DestructionMethod(str, Enum):
"""Only methods accepted as non-retrievable under 21 CFR Part 1317."""
INCINERATION = "INCINERATION"
CHEMICAL_DIGESTION = "CHEMICAL_DIGESTION"
ENCAPSULATION = "ENCAPSULATION"
REVERSE_DISTRIBUTION = "REVERSE_DISTRIBUTION" # transfer to authorized registrant
class Form41Error(ValueError):
"""Raised when a destruction event cannot support a Form 41 certification."""
@dataclass(frozen=True)
class WitnessedDestructionEvent:
"""A committed destruction event sourced from the append-only ledger."""
event_id: str
ndc: str
dosage_form: str # e.g. "tablet", "injectable_ml"
quantity: str # Decimal string
unit: str # explicit units, e.g. "mg", "mL", "tablet"
schedule: str
method: DestructionMethod
non_retrievable: bool # method attestation
actor_id: str
witness_1_id: str
witness_2_id: str
destroyed_at: str # ISO 8601 UTC
place: str
record_hash: str # the event's own chained hash
@dataclass(frozen=True)
class Form41LineItem:
ndc: str
dosage_form: str
unit: str
total_quantity: str
event_ids: tuple[str, ...]
@dataclass(frozen=True)
class Form41Record:
batch_id: str
line_items: tuple[Form41LineItem, ...]
methods: tuple[str, ...]
place: str
date_range: tuple[str, str]
witnesses: tuple[str, ...]
snapshot_hash: str
assembled_at: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
def _verify(event: WitnessedDestructionEvent) -> None:
"""Enforce the Form 41 evidentiary bar for one destruction event."""
if event.witness_1_id == event.witness_2_id:
raise Form41Error(f"{event.event_id}: two DISTINCT witnesses required.")
if event.actor_id in (event.witness_1_id, event.witness_2_id):
raise Form41Error(f"{event.event_id}: actor cannot also be a witness.")
if not event.non_retrievable:
raise Form41Error(
f"{event.event_id}: method {event.method.value} lacks non-retrievable "
"attestation under 21 CFR Part 1317."
)
if Decimal(event.quantity) <= 0 or not event.unit:
raise Form41Error(f"{event.event_id}: quantity and explicit unit required.")
def assemble_form_41(
batch_id: str, events: list[WitnessedDestructionEvent]
) -> Form41Record:
"""Build a snapshot-bound Form 41 record from witnessed-destruction events."""
if not events:
raise Form41Error("No destruction events supplied for the batch.")
for event in events:
_verify(event)
# Aggregate quantities by (NDC, dosage_form, unit); mixed units never sum.
grouped: dict[tuple[str, str, str], list[WitnessedDestructionEvent]] = defaultdict(list)
for event in events:
grouped[(event.ndc, event.dosage_form, event.unit)].append(event)
line_items = tuple(
Form41LineItem(
ndc=ndc,
dosage_form=form,
unit=unit,
total_quantity=str(sum((Decimal(e.quantity) for e in evs), Decimal("0"))),
event_ids=tuple(sorted(e.event_id for e in evs)),
)
for (ndc, form, unit), evs in sorted(grouped.items())
)
dates = sorted(e.destroyed_at for e in events)
places = {e.place for e in events}
if len(places) != 1:
raise Form41Error("All events in a batch must share one place of destruction.")
witnesses = tuple(sorted({w for e in events
for w in (e.witness_1_id, e.witness_2_id)}))
methods = tuple(sorted({e.method.value for e in events}))
# Snapshot hash binds the certified content to an exact ledger state.
canonical = json.dumps(
{"batch_id": batch_id,
"line_items": [asdict(li) for li in line_items],
"methods": methods, "place": places.pop(),
"date_range": [dates[0], dates[-1]],
"source_hashes": sorted(e.record_hash for e in events)},
sort_keys=True,
)
snapshot_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
record = Form41Record(
batch_id=batch_id, line_items=line_items, methods=methods,
place=next(iter({e.place for e in events})),
date_range=(dates[0], dates[-1]), witnesses=witnesses,
snapshot_hash=snapshot_hash,
)
logger.info(
"form41_assembled",
extra={"batch_id": batch_id, "line_items": len(line_items),
"witnesses": len(witnesses), "snapshot_hash": snapshot_hash},
)
return record
Grouping by (ndc, dosage_form, unit) — never by NDC alone — is what prevents the silent unit error that plagues hand-typed forms: 500 mg and 500 mL must never land in the same total. Sorting every collection before hashing makes the snapshot hash deterministic, so the same set of events always yields the same certification digest regardless of input order.
Verification & testing
Two properties must be proven: that an event failing the evidentiary bar is rejected, and that the snapshot hash is stable and unit-safe. The block below exercises both, plus the aggregation.
import pytest
def _event(**over):
base = dict(
event_id="d1", ndc="00409120230", dosage_form="injectable_ml",
quantity="2", unit="mL", schedule="II",
method=DestructionMethod.INCINERATION, non_retrievable=True,
actor_id="rph-1", witness_1_id="w-2", witness_2_id="w-3",
destroyed_at="2026-07-15T14:00:00+00:00", place="Room B-1",
record_hash="a" * 64,
)
base.update(over)
return WitnessedDestructionEvent(**base)
def test_two_distinct_witnesses_required():
with pytest.raises(Form41Error):
assemble_form_41("b1", [_event(witness_2_id="w-2")]) # witnesses identical
def test_non_retrievable_attestation_required():
with pytest.raises(Form41Error):
assemble_form_41("b1", [_event(non_retrievable=False)])
def test_quantities_aggregate_and_units_do_not_cross():
rec = assemble_form_41("b1", [
_event(event_id="d1", quantity="2", unit="mL"),
_event(event_id="d2", quantity="3", unit="mL"),
_event(event_id="d3", quantity="5", unit="mg", dosage_form="tablet"),
])
totals = {(li.ndc, li.unit): li.total_quantity for li in rec.line_items}
assert totals[("00409120230", "mL")] == "5" # 2 + 3 mL summed
assert totals[("00409120230", "mg")] == "5" # mg kept separate
def test_snapshot_hash_is_stable_and_order_independent():
a = assemble_form_41("b1", [_event(event_id="d1"), _event(event_id="d2")])
b = assemble_form_41("b1", [_event(event_id="d2"), _event(event_id="d1")])
assert a.snapshot_hash == b.snapshot_hash # order-independent
The emitted log line is the compliance artifact tying the assembled form back to the ledger. It carries the batch, counts, and snapshot hash but no patient data:
INFO cs_ledger.form41 form41_assembled \
batch_id=b1 line_items=2 witnesses=2 snapshot_hash=4e9a...1f7c
Re-running assemble_form_41 over the same stored events and comparing the resulting snapshot_hash to the digest printed on the certified form is the inspection check: a mismatch proves the form no longer reflects the ledger, which is exactly the tamper signal a 21 CFR Part 1317 destruction record must be able to surface.
Gotchas & compliance pitfalls
- The two-witness omission is the classic failure. A destruction observed by one person — or by two records of the same identity — cannot support a Form 41 certification. The
_verifystep rejects identical witnesses and an actor doubling as a witness; do not “fix” a single-witness destruction by copying the name into both fields, which is precisely the falsification the distinctness check exists to catch. non_retrievableis an attestation, not a default. A method value alone does not prove the drug was rendered irretrievable; the boolean attestation records that the operator certified the non-retrievable end state. Assembling a form from events where that attestation is absent would certify something the ledger never established, so it is rejected.- Quantity units must be explicit and never cross-summed. Milligrams, milliliters, and dosage units are different quantities; summing across them produces a meaningless total that an auditor will flag immediately. Grouping by unit — and storing quantities as
Decimalstrings — keeps each line item coherent and avoids floating-point drift on partial-unit destructions. - Snapshot binding must cover source hashes. Hashing only the aggregated totals would let someone swap the underlying events while keeping the same sums. Including the sorted
record_hashof every source event in the snapshot ties the certification to the exact ledger entries, so substituting events changes the snapshot hash. - One place, one batch. A Form 41 certifies destruction at a place; mixing destructions performed at different locations into one record misrepresents where the disposal occurred. The assembler refuses a batch spanning multiple places rather than silently picking one.
Frequently Asked Questions
Does generating the record file the Form 41 with the DEA?
No — this recipe produces the certifiable record, not a submission. Registrants generally retain the completed Form 41 as part of their required records and produce it on inspection or when the destruction involves DEA-witnessed or authorized processes; on-site destructions are recorded and kept rather than routinely mailed in. The value of assembling it from the ledger is that the retained record is provably consistent with the witnessed-destruction events, so it withstands inspection without manual reconciliation.
Why bind the record to a snapshot hash instead of just storing it?
Because a stored document can be edited, but a stored document plus a hash of its exact content cannot be edited undetectably. The snapshot hash — computed over the sorted line items and the source events’ own hashes — lets anyone recompute it later and prove the certified figures match the ledger state at assembly time. It is the same immutability guarantee the append-only ledger provides, extended to the derived form.
How does reverse distribution appear on the form?
As a destruction method value of REVERSE_DISTRIBUTION with its own non-retrievable attestation supplied by the authorized registrant that performed the destruction. The transfer out of the registrant’s custody is a disposition in the ledger, and the destruction attestation arrives from the receiving party; the assembler treats it like any other method provided the two-witness and non-retrievable checks are satisfied by the sourced events.
What keeps patient information off the Form 41?
The witnessed-destruction events carry only drug identity, quantity, method, and staff identities — never patient identifiers — so the assembled form inherits that boundary. This keeps the destruction record on the operational-compliance side of the 45 CFR § 164.502 segregation wall, exactly as the waste and vault-access records do, and means the certified Form 41 can be produced for a DEA inspection without exposing PHI.
Related
- Controlled Substance Waste & Destruction Logging — parent topic: the witnessed-waste and destruction lifecycle this record consumes
- Controlled Substance Storage & Handling Compliance — the append-only ledger and dual-control architecture behind the events
- Point-in-Time Inventory Reconstruction — reconstructing exact ledger state at a past instant using the same snapshot technique