Skip to content

MaLaC-HD library / API guide

Besides the malac-hd command-line tool, MaLaC-HD can be used directly from Python. This is useful when you want to generate mapping code as part of a build step, run transformations inside a larger application, or register your own model packages programmatically.

The workflow mirrors the two CLI modes:

  1. Convert a mapping (FML, or a FHIR StructureMap/ConceptMap resource) into standalone Python source code.
  2. Transform input data by running that generated code.

The examples below use sample mappings under tests/, so they run as-is from a source checkout of the repository. Those fixtures are not part of the installed PyPI package — when running against an installed malac-hd, substitute your own mapping and input files for the tests/... paths.

Note on versions. MaLaC-HD ships a generator per supported FHIR version. Import the r4 or r5 generator to match your mapping; the two expose the same API. The examples use r5; substitute r4 where noted.

Table of contents

Converting a mapping to Python

convert() turns a parsed mapping into a string of standalone Python source code. Parse the FML map with the matching generator, then call convert():

import os
from malac.hd.core.fhir.r5.generator import fml
# for an R4 mapping: from malac.hd.core.fhir.r4.generator import fml

map_path = "tests/fml/r5/source_cardinality/sourceCardinality2.map"

structure_map = fml.parse(map_path, silence=True)
py_code = structure_map.convert(silent=True, source=os.path.abspath(map_path))

# py_code is a complete, self-contained Python module as a string
print(len(py_code), "characters of generated Python")

parse(path, silence=True) reads the FML file (silence=True suppresses the echo of the parsed map). convert() accepts:

argument default meaning
silent True when False, prints the generated Python to stdout while converting
source "." absolute path to the mapping, embedded in the generated code as its origin (used for error messages); pass os.path.abspath(map_path)
standalone True emit a runnable module (with if __name__ == "__main__") rather than a fragment

To persist the generated module, just write the string to a file:

with open("mapping.py", "w", newline="", encoding="utf-8") as f:
    f.write(py_code)

This is the library equivalent of malac-hd -m map.5.map -co mapping.py.

Running a transformation

The generated code exposes a transform(source_path, target_path) function. Load the code as a module and call it. You can load from the in-memory string (no file needed):

import importlib.util

spec = importlib.util.spec_from_loader("mapping", loader=None)
module = importlib.util.module_from_spec(spec)
exec(py_code, module.__dict__)          # py_code from convert() above

module.transform(
    "tests/fml/r5/source_cardinality/patient_1name.xml",   # source
    "out.xml",                                             # target
)

...or, if you saved the module to mapping.py and it is on sys.path:

import mapping
mapping.transform("patient.xml", "out.xml")

The format is detected from the file extension of both paths: .xml reads and writes FHIR XML, .json reads and writes FHIR JSON (JSON is available where the source/target model supports it). The resource type is detected from the document itself, so no type argument is needed.

This is the library equivalent of python mapping.py -s patient.xml -t out.xml.

Reading and writing streams

transform() takes two further optional arguments, source_type and target_type ("xml" or "json"), and accepts "-" in place of either path to read from stdin / write to stdout:

module.transform("-", "-", source_type="xml", target_type="xml")

The format cannot be sniffed from a stream, so the matching *_type is required whenever the path is "-" — otherwise transform() raises ValueError. For real file paths the two arguments are optional and override the extension-based detection:

# a .txt file that actually holds FHIR JSON
module.transform("payload.txt", "out.xml", source_type="json")

Passing source_type="json" for a mapping whose model is XML-only (CDA, for example) also raises ValueError. Progress messages go to stderr, so a transform writing to stdout emits only the payload — which is what lets generated scripts be chained in a shell pipeline:

cat patient.xml | python step1.py -s - -t - -st xml -tt xml | python step2.py -s - -t out.xml -st xml

This is the library equivalent of python mapping.py -s - -t - -st xml -tt xml.

Convert and transform in one step

For quick testing you can chain both phases without ever touching disk for the generated code:

import importlib.util, os
from malac.hd.core.fhir.r5.generator import fml

def compile_map(map_path):
    structure_map = fml.parse(map_path, silence=True)
    py_code = structure_map.convert(silent=True, source=os.path.abspath(map_path))
    spec = importlib.util.spec_from_loader("mapping", loader=None)
    module = importlib.util.module_from_spec(spec)
    exec(py_code, module.__dict__)
    return module

mapping = compile_map("tests/fml/r5/source_cardinality/sourceCardinality2.map")
mapping.transform("tests/fml/r5/source_cardinality/patient_1name.xml", "out.xml")

This mirrors malac-hd -m map.5.map -ti patient.xml -to out.xml (convert, then immediately transform).

Converting a StructureMap resource instead of FML

The same generators can consume a FHIR StructureMap resource directly instead of FML. malac.hd.list_m_modules maps a mapping file's extension to the generator module that handles it:

import malac.hd
print(malac.hd.list_m_modules[".5.fhir.xml"])   # 'fhir.r5.generator.structuremap'
print(malac.hd.list_m_modules[".map"])           # ['fhir.r5...fml', 'fhir.r4...fml']

Parse the StructureMap with that generator and call convert() exactly as above. For an XML resource:

from malac.hd.core.fhir.r5.generator import structuremap
structure_map = structuremap.parse("map.5.fhir.xml", silence=True)
py_code = structure_map.convert(silent=True, source="map.5.fhir.xml")

When an extension maps to a list of candidate generators (e.g. plain .map or .xml, which could be r4 or r5), the CLI tries each and keeps the best-matching one. In library code, pick the generator for the version you know you have, or replicate that selection logic (see malac/hd/__main__.py).

That choice is about which generator parses your mapping. Which FHIR model the generated code is built against is decided separately, by malac.hd.core.fhir.base.generator.model.select_fhir_module: the target model wins, then the source model, and a mapping between two non-FHIR models falls back to the FHIR version of the generator in use. The generator exposes the result as fhir_module, next to i_is_fhir / o_is_fhir for the separate question of whether a given side of the mapping is FHIR at all. Mappings that cross FHIR versions (an R4-to-R5 map, say) therefore compile against one model consistently, without the caller having to arrange it.

Registering model packages

Mappings reference their source/target models through uses URLs. MaLaC-HD resolves those URLs against malac.hd.list_i_o_modules. The built-in FHIR (and optional CDA) models are registered automatically; to add your own model packages (for example project-specific logical models), register them before converting so the uses URLs resolve:

import malac.hd

# module names and/or paths to .py files, each exposing `list_i_o_modules`
malac.hd.load_model_packages(["my_models_plugin", "/path/to/other_models.py"])

# or from the MALAC_HD_MODELS environment variable (comma- or os.pathsep-separated)
malac.hd.load_model_packages_from_env()

A model package is any importable Python module exposing a list_i_o_modules dict that maps canonical uses-URL prefixes to the module holding the model classes — the same contract malac.models.fhir fulfils. See Model package plugins in the README for the plugin contract and CLI (-mod/--models) equivalents.

load_model_packages() raises if a requested package cannot be imported or does not expose list_i_o_modules — an explicitly requested package must load. It returns the list of loaded modules.

Error handling

Every error MaLaC-HD raises derives from malac.hd.exceptions.MalacHDError, so a caller can catch the whole family without also catching KeyboardInterrupt / SystemExit:

from malac.hd.exceptions import MalacHDError
from malac.hd.core.fhir.r5.generator import fml

try:
    py_code = fml.parse(map_path, silence=True).convert(silent=True, source=map_path)
except MalacHDError as err:
    print(f"Could not compile the mapping: {err}")

Catch a specific subclass to react to one kind of problem:

Exception Raised when
MappingClassificationError the mapping's rule language / FHIR version can't be determined
ModuleResolutionError a uses URL has no registered source/target model
MappingImportError an imported mapping/resource file can't be found
GroupResolutionError a StructureMap group can't be resolved or is invalid
UnsupportedMappingError the mapping uses a construct MaLaC-HD can't compile
FHIRPathError a FHIRPath expression can't be compiled
ConceptMapError a ConceptMap can't be converted

All of these subclass MappingError, which is itself a MalacHDError.

The malac-hd command-line tool catches these and prints a clean Error: <message> to stderr with a non-zero exit code, instead of a Python traceback.

Utilities

import malac.hd
malac.hd.version()   # e.g. "1.8.0+20260915-dev" or "1.8.0" for an external build

CLI to API cheatsheet

CLI Library
malac-hd -m map.5.map -co out.py code = fml.parse("map.5.map", silence=True).convert(...) then write it
python out.py -s in.xml -t out.xml module.transform("in.xml", "out.xml")
python out.py -s - -t - -st xml -tt xml module.transform("-", "-", source_type="xml", target_type="xml")
malac-hd -m map.5.map -ti in.xml -to out.xml convert + transform in one step
malac-hd ... -mod my_plugin.py malac.hd.load_model_packages(["my_plugin.py"])
MALAC_HD_MODELS=... malac-hd ... malac.hd.load_model_packages_from_env()

For the full CLI reference, see the README.