FHIR development in Python usually means wrestling with raw dicts, guessing field names from a massive spec, and discovering typos only at runtime. We built a generator that eliminates all of that.
@atomic-ehr/codegen produces strongly typed Pydantic models from any FHIR package — with validation, IDE autocomplete, and optional fhirpy integration. It works with any FHIR server, not just Aidbox. The project is open source (MIT), and we welcome contributions.
This post updates our earlier Python SDK article. Since then, the generator has been rewritten from scratch in a single TypeScript stack as @atomic-ehr/codegen.
What you get:
- Pydantic v2 models for every resource and data type
- Value sets —
Literalenums and genericCodeableConcept[T]with bindings in the type - Primitive extensions — typed
_birthDate,_familyfields - fhirpy async client — plug directly into
AsyncFHIRClient - Any FHIR packages — R4, US Core, and custom in one pass; patch packages on the fly
- Type Schema — tree shaking, logical model promotion, collision resolution
Generate Your FHIR Types
Install the generator (bun add and pnpm add work too):
npm install @atomic-ehr/codegen
Create a generate.ts script:
import { APIBuilder } from "@atomic-ehr/codegen";
const main = async () => {
const builder = new APIBuilder()
.fromPackage("hl7.fhir.r4.core", "4.0.1")
.python({
fieldFormat: "snake_case",
allowExtraFields: false,
primitiveTypeExtension: true,
})
.typeSchema({
treeShake: {
"hl7.fhir.r4.core": {
"http://hl7.org/fhir/StructureDefinition/Patient": {},
"http://hl7.org/fhir/StructureDefinition/Observation": {},
"http://hl7.org/fhir/StructureDefinition/Bundle": {},
},
},
})
.outputTo("./fhir_types")
.cleanOutput(true);
const report = await builder.generate();
if (!report.success) process.exit(1);
};
main();
Run it:
npx tsx generate.ts
You now have a ready-to-use Python package in ./fhir_types/. Tree shaking pulls in only the resources you listed plus their dependencies (DomainResource, Element, OperationOutcome, etc.) automatically.
What Gets Generated
fhir_types/
├── __init__.py # Exports + model_rebuild() for forward refs
├── requirements.txt # pydantic, requests, fhirpy, mypy, pytest
├── README.md
└── hl7_fhir_r4_core/
├── __init__.py
├── base.py # Complex types: CodeableConcept, HumanName, ...
├── patient.py # Patient + nested types (PatientContact, ...)
├── observation.py
├── operation_outcome.py
├── bundle.py
├── domain_resource.py
├── resource.py
└── resource_families.py # Polymorphic validators for Bundle.entry etc.
Each resource is a Pydantic model with proper field types, aliases, and validation:
# Simplified -- other fields also carry serialization_alias, omitted for brevity
class Patient(DomainResource):
model_config = ConfigDict(
validate_by_name=True,
serialize_by_alias=True,
extra="forbid"
)
resource_type: Literal['Patient'] = Field(
default='Patient', alias='resourceType',
serialization_alias='resourceType', frozen=True
)
birth_date: str | None = Field(None, alias="birthDate")
birth_date_extension: Element | None = Field(None, alias="_birthDate")
gender: Literal["male", "female", "other", "unknown"] | None = Field(None)
marital_status: CodeableConcept[
Literal["A", "D", "I", "L", "M", "P", "S", "T", "U", "W", "UNK"] | str
] | None = Field(None, alias="maritalStatus")
name: PyList[HumanName] | None = Field(None) # PyList = typing.List
# ...
See full generated file for the complete Patient model.
Create and Work with Resources
Setup Python environment (Python 3.12+)
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r fhir_types/requirements.txt
The generator produces types and validation — no HTTP client is bundled. For server communication, use fhirpy or any HTTP library. Here we work with models directly:
from fhir_types.hl7_fhir_r4_core import Element, Extension, HumanName, Patient
patient = Patient(
name=[HumanName(given=["John"], family="Doe")],
gender="male",
birth_date="1980-01-01",
birth_date_extension=Element(
extension=[
Extension(
url="http://hl7.org/fhir/StructureDefinition/patient-birthTime",
value_date_time="1980-01-01T08:30:00-05:00",
),
],
),
)
# Serialize to FHIR JSON (camelCase keys, nulls excluded)
print(patient.to_json(indent=2))
{
"resourceType": "Patient",
"birthDate": "1980-01-01",
"_birthDate": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/patient-birthTime",
"valueDateTime": "1980-01-01T08:30:00-05:00"
}]
},
"gender": "male",
"name": [{ "family": "Doe", "given": ["John"] }]
}
# Round-trip: JSON -> Patient
restored = Patient.from_json(patient.to_json())
assert restored == patient
# Typed field access
patient.gender # 'male'
assert patient.name is not None
patient.name[0].family # 'Doe'
patient.birth_date # '1980-01-01'
assert patient.birth_date_extension is not None
assert patient.birth_date_extension.extension is not None
patient.birth_date_extension.extension[0].value_date_time # '1980-01-01T08:30:00-05:00'
Type Checking and Validation
What happens when we make mistakes?
from fhir_types.hl7_fhir_r4_core import HumanName, Patient
Patient(
name=[HumanName(family="Doe")],
gender="FOO", # wrong value
some_data="1990-01-01", # wrong field
)
Static analysis with mypy. Add the Pydantic plugin to mypy.ini:
[mypy]
python_version = 3.12
plugins = pydantic.mypy
[pydantic-mypy]
init_typed = True
Then run:
$ mypy . --strict
main.py:10: error: Unexpected keyword argument "some_data" for "Patient" [call-arg]
main.py:12: error: Argument "gender" to "Patient" has incompatible type "Literal['FOO']"; expected "Literal['male', 'female', 'other', 'unknown'] | None" [arg-type]
Runtime validation — run the script and Pydantic catches both errors at instantiation:
$ python main.py
pydantic_core._pydantic_core.ValidationError: 2 validation errors for Patient
gender
Input should be 'male', 'female', 'other' or 'unknown'
some_data
Extra inputs are not permitted
No need to run a validator separately — models enforce constraints on construction. If you need to accept extra fields, generate with allowExtraFields: true.
Customization Options
| Option | Type | Default | Effect |
|---|---|---|---|
fieldFormat | "snake_case" | "camelCase" | "snake_case" | Field naming: birth_date vs birthDate |
allowExtraFields | boolean | false | Accept unknown fields in JSON input |
primitiveTypeExtension | boolean | false | Generate typed _birthDate, _family companion fields |
fhirpyClient | boolean | false | fhirpy-compatible base class and resourceType |
These options combine with APIBuilder-level features — tree shaking to include only the resources you need, multi-package loading to mix R4 core with IGs and custom profiles, and package preprocessing to patch broken upstream packages before generation.
Coming Next: Profile Support for Python
Our TypeScript generator already supports FHIR profile classes — generated classes that auto-populate fixed values, provide typed accessors for slices and extensions, and include client-side validation. We're working on bringing the same capabilities to Python:
- Slice accessors with discriminator values applied automatically
- Extension getters/setters
- Validation returning structured errors and warnings
If you're interested in this — feedback on what profile patterns matter most for your Python workflows is very welcome on GitHub.
Beyond Python: What Else @atomic-ehr/codegen Does
This post focused on the Python generator, but @atomic-ehr/codegen is a multi-language code generation platform. Here's what else it offers:
- Four built-in generators — Python, TypeScript, C#, and a Mustache template engine for any language
- Package preprocessing — real-world FHIR packages often ship with bugs: malformed canonical URLs, missing CodeSystem concepts, unavailable external ValueSets. The
preprocessPackagehook lets you patch these before generation, so you don't have to fork the package or work around errors downstream - Logical model promotion — convert logical StructureDefinitions into first-class resources for code generation
- Schema collision resolution — when multiple packages define overlapping bindings, you can specify which source wins
- Multi-source loading — combine NPM packages, local TGZ archives, and loose StructureDefinition JSON files in a single generation pass
Working Python examples:
- Python + Pydantic — snake_case, primitive extensions, custom sync client
- Python + fhirpy — camelCase, async fhirpy client
@atomic-ehr/codegen is open source under the MIT license. If you're working with FHIR in Python — whether you're building data pipelines, ML features, or clinical apps — give it a try and let us know what you think. Issues, feature requests, and PRs are all welcome.




