---
{
  "title": "@atomic-ehr/codegen: Python FHIR Types",
  "description": "Generate strongly typed Pydantic models from any FHIR packages with @atomic-ehr/codegen — validation, IDE support, polymorphic bundles, primitive extensions, and fhirpy integration included.",
  "date": "2026-04-20",
  "author": "Aleksandr Penskoi",
  "reading-time": "6 minutes",
  "tags": [
    "FHIR Tools",
    "FHIR Standard",
    "Code Generation",
    "Python"
  ]
}
---
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`](https://github.com/atomic-ehr/codegen) produces strongly typed [Pydantic](https://docs.pydantic.dev/latest/) models from any FHIR package — with validation, IDE autocomplete, and optional [fhirpy](https://github.com/beda-software/fhir-py) integration. It works with any FHIR server, not just [Aidbox](https://www.health-samurai.io/aidbox). The project is open source (MIT), and we welcome contributions.

This post updates our [earlier Python SDK article](https://www.health-samurai.io/articles/type-schema-python-sdk-for-fhir). 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** — `Literal` enums and generic `CodeableConcept[T]` with bindings in the type
- **Primitive extensions** — typed `_birthDate`, `_family` fields
- **[fhirpy async client](https://github.com/beda-software/fhir-py)** — plug directly into `AsyncFHIRClient`
- **[Any FHIR packages](https://github.com/atomic-ehr/fhir-canonical-manager)** — R4, US Core, and custom in one pass; patch packages on the fly
- **[Type Schema](https://www.health-samurai.io/articles/type-schema-a-pragmatic-approach-to-build-fhir-sdk)** — tree shaking, logical model promotion, collision resolution

## Generate Your FHIR Types

Install the generator (`bun add` and `pnpm add` work too):

```bash
npm install @atomic-ehr/codegen
```

Create a `generate.ts` script:

```typescript
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:

```bash
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:

```python
# 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](https://github.com/atomic-ehr/codegen/blob/main/examples/python/fhir_types/hl7_fhir_r4_core/patient.py) for the complete Patient model.

## Create and Work with Resources

<details>
<summary>Setup Python environment (Python 3.12+)</summary>

```bash
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r fhir_types/requirements.txt
```

</details>

The generator produces **types and validation** — no HTTP client is bundled. For server communication, use [fhirpy](https://github.com/beda-software/fhir-py) or any HTTP library. Here we work with models directly:

```python
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))
```

```json
{
  "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"] }]
}
```

```python
# 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?

```python
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`:

```ini
[mypy]
python_version = 3.12
plugins = pydantic.mypy

[pydantic-mypy]
init_typed = True
```

Then run:

```bash
$ 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:

```bash
$ 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](https://github.com/atomic-ehr/codegen/tree/main/examples/python-fhirpy)-compatible base class and resourceType |

These options combine with `APIBuilder`-level features — [tree shaking](#generate-your-fhir-types) to include only the resources you need, [multi-package loading](https://github.com/atomic-ehr/fhir-canonical-manager) to mix R4 core with IGs and custom profiles, and [package preprocessing](https://github.com/atomic-ehr/codegen#architecture) to patch broken upstream packages before generation.

## Coming Next: Profile Support for Python

Our TypeScript generator already supports [FHIR profile classes](https://github.com/atomic-ehr/codegen/blob/main/docs/posts/2026-03-09-typescript-profiles-quick.md) — 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](https://github.com/atomic-ehr/codegen).

## 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 `preprocessPackage` hook 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](https://github.com/atomic-ehr/codegen/tree/main/examples/python) — snake_case, primitive extensions, custom sync client
- [Python + fhirpy](https://github.com/atomic-ehr/codegen/tree/main/examples/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.

[GitHub](https://github.com/atomic-ehr/codegen) | [NPM](https://www.npmjs.com/package/@atomic-ehr/codegen) | [Aleksandr Penskoi on LinkedIn](https://www.linkedin.com/in/aleksandr-penskoi/)
