Skip to main content

Pydantic-powered HL7 v2 models with ER7 parsing and XML serialisation

Project description

hl7types

PyPI version Supported Python Versions Documentation codecov Downloads

For full API reference, usage guides, and HL7 type documentation, visit hl7types.readthedocs.io.

HL7 v2 (Health Level Seven Version 2) is the most widely deployed healthcare messaging standard in the world, used to exchange clinical and administrative data between hospital systems, laboratories, pharmacies, and other healthcare applications. It defines a pipe-delimited wire format (ER7) for encoding messages such as patient admissions, lab results, and order requests, and is the backbone of interoperability in the majority of existing healthcare infrastructure globally.

hl7types provides typed, Pydantic-powered Python models for HL7 v2 messages, segments, and datatypes, with built-in ER7 and XML serialisation. Models are generated directly from the HL7 specification, giving you full type safety, IDE autocompletion, and validation out of the box.

With thanks to the fhir.resources project for demonstrating what modern, Pydantic-powered healthcare data tooling can look like.

All HL7 v2 model classes are generated using hl7-parser.

HL7® is a registered trademark owned by Health Level Seven International.

Version info

Each HL7 version is available as a sub-package under hl7types.hl7.<version>, where the version string uses underscores in place of dots , so v2.5.1 is at hl7types.hl7.v2_5_1 and v2.1 is at hl7types.hl7.v2_1.

Available versions:

  • v2.1 (hl7types.hl7.v2_1)
  • v2.2 (hl7types.hl7.v2_2)
  • v2.3 (hl7types.hl7.v2_3)
  • v2.3.1 (hl7types.hl7.v2_3_1)
  • v2.4 (hl7types.hl7.v2_4)
  • v2.5 (hl7types.hl7.v2_5)
  • v2.5.1 (hl7types.hl7.v2_5_1)
  • v2.6 (hl7types.hl7.v2_6)
  • v2.7 (hl7types.hl7.v2_7)
  • v2.7.1 (hl7types.hl7.v2_7_1)
  • v2.8 (hl7types.hl7.v2_8)
  • v2.8.1 (hl7types.hl7.v2_8_1)
  • v2.8.2 (hl7types.hl7.v2_8_2)

Installation

pip install hl7types

Real-world examples

For workflow-based examples covering common HL7 integration tasks, parsing ADT and ORU messages, building ACKs, handling missing segments, custom Z-segments, XML conversion, and conformance-profile validation, see the examples/ directory.

Usage

Example 1: Construct an ADT^A01 message from typed models and access fields directly:

>>> from hl7types.hl7.v2_5_1.datatypes import CE, CX, FN, HD, MSG, PT, SAD, TS, VID, XAD, XPN, XTN
>>> from hl7types.hl7.v2_5_1.messages import ADT_A01
>>> from hl7types.hl7.v2_5_1.segments import EVN, MSH, PID, PV1
>>> msh = MSH(
...     msh_3=HD(hd_1="HL7TYPES"),
...     msh_4=HD(hd_1="YSBYTY PONTYPANDY"),
...     msh_5=HD(hd_1="WPAS"),
...     msh_6=HD(hd_1="YSBYTY CWM YSWYTH"),
...     msh_7=TS(ts_1="20240101090000"),
...     msh_9=MSG(msg_1="ADT", msg_2="A01"),
...     msh_10="MSG000001",
...     msh_11=PT(pt_1="P"),
...     msh_12=VID(vid_1="2.5.1"),
...     msh_17="CYM",
... )
>>> evn = EVN(evn_2=TS(ts_1="20260101000000"), evn_3=TS(ts_1="20240101090000"))
>>> pid = PID(
...     pid_3=[CX(cx_1="M12345", cx_4=HD(hd_1="WPAS"), cx_5="PI")],
...     pid_5=[XPN(xpn_1=FN(fn_1="Jones"), xpn_2="Brynn", xpn_5="Mr")],
...     pid_7=TS(ts_1="19930707"),
...     pid_8="M",
...     pid_11=[XAD(xad_1=SAD(sad_1="18 Stryd Mill"), xad_3="Pontypandy", xad_5="CF41 7ZQ", xad_6="CYM", xad_7="H")],
...     pid_13=[XTN(xtn_1="0118 999 881 999 119 725 3", xtn_2="PRN", xtn_3="PH")],
...     pid_15=CE(ce_1="cy", ce_2="Cymraeg"),
... )
>>> msg = ADT_A01(MSH=msh, EVN=evn, PID=pid, PV1=PV1(pv1_2="I"))
>>> isinstance(msg.PID.pid_5[0], XPN)
True
>>> msg.PID.pid_5[0].xpn_1.fn_1
'Jones'
>>> msg.PID.pid_8
'M'

Example 2: Encode to ER7 (pipe-delimited):

>>> er7 = msg.model_dump_er7()
>>> for segment in er7.split("\r"):
...     print(segment)
MSH|^~\&|HL7TYPES|YSBYTY PONTYPANDY|WPAS|YSBYTY CWM YSWYTH|20240101090000||ADT^A01|MSG000001|P|2.5.1|||||CYM
EVN||20260101000000|20240101090000
PID|||M12345^^^WPAS^PI||Jones^Brynn^^^Mr||19930707|M|||18 Stryd Mill^^Pontypandy^Rhondda Cynon Taf^CF41 7ZQ^CYM^H||0118 999 881 999 119 725 3^PRN^PH||cy^Cymraeg
PV1||I

Example 3: Decode an ER7 string back into typed models:

>>> decoded = ADT_A01.model_validate_er7(er7)
>>> isinstance(decoded.PID.pid_5[0], XPN)
True
>>> decoded.PID.pid_5[0].xpn_1.fn_1
'Jones'
>>> decoded.PID.pid_8
'M'

Example 4: Standard Pydantic serialisation is available on all models:

>>> import json
>>> data = json.loads(msg.PID.model_dump_json())
>>> data["PID.8"]
'M'

Example 5: Type validation is enforced , passing the wrong type raises a ValidationError:

>>> from pydantic import ValidationError
>>> try:
...     PID(pid_3="not-a-list")
... except ValidationError as e:
...     print(e)
2 validation errors for PID
pid_3
  Input should be a valid list [type=list_type, input_value='not-a-list', input_type=str]
    For further information visit https://errors.pydantic.dev/2.13/v/list_type
pid_5
  Field required [type=missing, input_value={'pid_3': 'not-a-list'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.13/v/missing

Example 6: Fields accept human-readable aliases as well as the positional names , all three forms are equivalent:

>>> from hl7types.hl7.v2_5_1.segments import MSH
>>> from hl7types.hl7.v2_5_1.datatypes import HD
>>> # these are all the same field
>>> MSH(msh_3=HD(hd_1="HL7TYPES")).msh_3.hd_1
'HL7TYPES'
>>> MSH(sending_application=HD(hd_1="HL7TYPES")).msh_3.hd_1
'HL7TYPES'
>>> MSH(**{"MSH.3": HD(hd_1="HL7TYPES")}).msh_3.hd_1
'HL7TYPES'

Example 7: By default, decoding is strict. A required segment absent from the wire raises a ValidationError. Pass strict=False to opt into lenient mode, where missing required segments are filled with empty placeholders instead:

>>> from hl7types import decode_er7
>>> from hl7types.hl7.v2_5_1.messages import ACK
>>> from pydantic import ValidationError
>>>
>>> # ACK wire with MSA omitted
>>> wire = "MSH|^~\\&|SEND|FAC|RECV|FAC|20240101120000||ACK|MSG001|P|2.5.1\r"
>>>
>>> # Strict (default): raises if a required segment is missing
>>> try:
...     decode_er7(wire, msg_cls=ACK)
... except ValidationError as e:
...     print(e)
1 validation error for ACK
MSA
  Field required [type=missing, ...]
>>>
>>> # Lenient (opt-in): succeeds, msg.MSA is an empty placeholder
>>> import warnings
>>> with warnings.catch_warnings(record=True):
...     msg = decode_er7(wire, msg_cls=ACK, strict=False)
>>> msg.MSA.model_fields_set
set()

strict=False is also accepted by model_validate_er7:

>>> ACK.model_validate_er7(wire)  # raises ValidationError (strict by default)
>>> with warnings.catch_warnings(record=True):
...     msg = ACK.model_validate_er7(wire, strict=False)  # succeeds

Example 8: Encode to XML:

>>> print(msg.model_dump_xml())
<?xml version="1.0" encoding="UTF-8"?><ADT_A01 xmlns="urn:hl7-org:v2xml">
    <MSH>
        <MSH.1>|</MSH.1>
        <MSH.2>^~\&amp;</MSH.2>
        <MSH.3>
            ...

Custom segments and the registry

Real-world HL7 deployments frequently extend the standard with vendor-specific Z-segments (e.g. ZWCC, ZPID). The HL7Registry lets you register custom segment and message models so they are recognised during encoding and decoding without touching the generated code.

Example 9: Define a custom Z-segment and encode a message that includes it:

>>> from typing import Optional
>>> from pydantic import Field
>>> from hl7types import HL7Registry
>>> from hl7types.hl7 import HL7Model
>>> from hl7types.hl7.v2_5_1.segments import MSA, MSH
>>> from hl7types.hl7.v2_5_1.datatypes import HD, MSG, PT, TS, VID
>>>
>>> class ZWCC(HL7Model):
...     zwcc_1: Optional[str] = Field(None, serialization_alias="ZWCC.1")
...     zwcc_2: Optional[str] = Field(None, serialization_alias="ZWCC.2")
...
>>> _ZWCC = ZWCC
>>>
>>> class WCCACKMessage(HL7Model):
...     MSH: MSH
...     MSA: MSA
...     ZWCC: Optional[_ZWCC] = None
...
>>> msh = MSH(
...     msh_3=HD(hd_1="WPAS"), msh_5=HD(hd_1="SEND"),
...     msh_7=TS(ts_1="20260101120000"), msh_9=MSG(msg_1="ACK"),
...     msh_10="MSG000002", msh_11=PT(pt_1="P"), msh_12=VID(vid_1="2.5.1"),
... )
>>> msg = WCCACKMessage(MSH=msh, MSA=MSA(msa_1="AA", msa_2="MSG000001"), ZWCC=ZWCC(zwcc_1="WPAS", zwcc_2="20260101120000"))
>>> for segment in msg.model_dump_er7().split("\r"):
...     print(segment)
MSH|^~\&|WPAS||SEND||20260101120000||ACK|MSG000002|P|2.5.1
MSA|AA|MSG000001
ZWCC|WPAS|20260101120000

Example 10: Decode a wire string containing a custom segment by registering the message class:

>>> from hl7types import decode_er7
>>>
>>> wire = (
...     "MSH|^~\\&|WPAS||SEND||20260101120000||ACK|MSG000002|P|2.5.1\r"
...     "MSA|AA|MSG000001\r"
...     "ZWCC|WPAS|20260101120000\r"
... )
>>>
>>> registry = HL7Registry()
>>> registry.register_segment("ZWCC", ZWCC)
>>> registry.register_message("2.5.1", "ACK", WCCACKMessage)
>>>
>>> msg = decode_er7(wire, registry=registry)
>>> msg.ZWCC.zwcc_1
'WPAS'

By default MSH, FHS, and BHS cannot be overridden. The decoder relies on them internally to detect encoding characters and message type. Attempting to register them raises immediately:

>>> registry.register_segment("MSH", ZWCC)
ValueError: 'MSH' is a delimiter-definition segment and cannot be overridden

Each HL7Registry instance is independent, so concurrent pipelines with different vendor extensions can each hold their own registry without shared state.

Conformance profiles

HL7 v2 conformance profiles constrain a base message specification for a specific integration, declaring exactly which fields are required, which are forbidden, and what coded values are permitted. hl7types can parse a profile XML file and apply its constraints to the standard segment classes via the registry, so the profile rules are enforced automatically during normal Pydantic validation.

Example 11: Parse a conformance profile and validate a message against it:

>>> from hl7types import HL7Registry, decode_er7
>>> from hl7types.profiles.builder import build_registry_from_profile
>>> from hl7types.profiles.parser import parse_profile, parse_tables
>>>
>>> tables = parse_tables("/path/to/sampleTables.xml")
>>> profile = parse_profile("/path/to/ADT_A01.xml")
>>>
>>> registry = HL7Registry()
>>> build_registry_from_profile(profile, registry, tables=tables)
>>>
>>> msg = decode_er7(wire, registry=registry, strict=True)

Constrained segment classes can also be used directly for programmatic construction:

>>> ConformedPID = registry.get_segment("PID")
>>> pid = ConformedPID(
...     pid_3=[CX(cx_1="MRN001", cx_4=HD(hd_1="HospA"), cx_5="MR")],
...     pid_5=[XPN(xpn_1=FN(fn_1="Smith"), xpn_2="John")],
...     pid_8="M",
... )

Any field that violates the profile raises a ValidationError immediately. See the conformance profiles documentation for full details.

Error handling

When validation fails, errs_from_exception converts a Pydantic ValidationError into a list of spec-compliant ERR segments ready to embed in a negative acknowledgement. It handles the structural differences between HL7 versions automatically.

Example 12: Build a NAK from a validation failure:

>>> from hl7types.utils.error import errs_from_exception
>>>
>>> try:
...     decode_er7(wire, registry=registry, strict=True)
... except Exception as e:
...     errs = errs_from_exception(e, "2.8.2")
...     nak = ACK(MSH=msh, MSA=MSA(msa_1="AE", msa_2="MSG000001"), ERR=errs)
...     print(nak.model_dump_er7())
MSH|^~\&|RECEIVING_APP||SENDING_APP||20260101120000||ACK|MSG000002|P|2.8.2
MSA|AE|MSG000001
ERR||PID^1^3|101^Required field missing^HL70357|E
ERR||PID^1^8|103^Table value not found^HL70357|E

The correct ERR structure is produced for every supported HL7 version, from the simple CM string of v2.1 through to the fully structured CWE/ERL form of v2.4 and later. See the error handling documentation for full details.

License

Proudly licensed under the MIT License. See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hl7types-0.7.0.tar.gz (3.9 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hl7types-0.7.0-py3-none-any.whl (10.6 MB view details)

Uploaded Python 3

File details

Details for the file hl7types-0.7.0.tar.gz.

File metadata

  • Download URL: hl7types-0.7.0.tar.gz
  • Upload date:
  • Size: 3.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for hl7types-0.7.0.tar.gz
Algorithm Hash digest
SHA256 ce1b647b5eb046d8393ba6a493d3f9700a99637f92d30cd8d198cdc60033cd5f
MD5 bf3404e304738c1fbdf71bd349431dbe
BLAKE2b-256 e80d159dca2bedd0980280b424bba7277fb21296d819ef8ed890d3e6de9a059c

See more details on using hashes here.

File details

Details for the file hl7types-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: hl7types-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 10.6 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for hl7types-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3dba8ae83ce712099d489df9cd4ce2d66ff983d4fc0fbfeb6d5ce91d725fd3ba
MD5 8e68cc4a2b5bd3d1cd6947ff2eaf3d43
BLAKE2b-256 501a703be1e097d43319629239b2a4e516f4e12ed17cd7194047ea329385fe20

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page