Analyzing FHIR Data in a Tabular Format With Python

Not yet updated for 2026
Roles: Informaticist
Learning objectives
  1. Understand the high-level approaches for converting FHIR-formatted data into tabular for analysis in Python.
  2. Learn how the FHIR-PYrate library facilitates requesting data from a FHIR server, and creating tidy tabular data tables.

Data analysis approaches in Python often use Pandas DataFrames to store tabular data. There are two primary approaches to loading FHIR-formatted data into Pandas DataFrames:

  1. Writing Python code to manually convert FHIR instances in JSON format into DataFrames.

    This does not require any special skills beyond data manipulation in Python, but in practice can be laborious (especially with large number of data elements) and prone to bugs.

  2. Using a purpose-built library like FHIR-PYrate to automatically convert FHIR instances into DataFrames.

    It is recommended to try this approach first, and only fall back to (1) if needed.

To use FHIR-PYrate, you will need a Python 3 runtime with FHIR-PYrate and Pandas installed.

1 FHIR testing server

The examples in this module use a FHIR testing server populated with Synthea data in FHIR R4 format via public HAPI Test Server operated by HAPI FHIR.

The endpoint for this testing server is:

https://hapi.fhir.org/baseR4

However, any FHIR server loaded with testing data can be used. See Standing up a FHIR Testing Server for instructions to set up your own test server.

The code blocks in the following section show sample output immediately after. This is similar to the code cells and results in a Jupyter notebook.

2 Retrieving FHIR data

Once your environment is set up, you can run the following Python code to retrieve instances of the Patient resource from a test server:

# Load dependencies
from fhir_pyrate import Pirate
import pandas as pd

# Instantiate a Pirate object using the FHIR-PYrate library to query a test FHIR server
search = Pirate(
    auth=None,
    base_url="https://hapi.fhir.org/baseR4",
    print_request_url=True,
)

# Use the whimsically named `steal_bundles()` method to instantiate a search interaction
#
# For more information, see https://github.com/UMEssen/FHIR-PYrate/#pirate
bundles = search.steal_bundles(
    resource_type="Patient",
    request_params={
        "_count": 10,  # Get 10 instances per page
        "identifier": "https://github.com/synthetichealth/synthea|",
    },
    num_pages=1,  # Get 1 page (so a total of 10 instances)
)

# Execute the search and convert to a Pandas DataFrame
df = search.bundles_to_dataframe(bundles)

df.head(5)
/home/runner/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhirpathpy/engine/nodes.py:7: SyntaxWarning: "\." is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\."? A raw string is also an option.
  timeRE = '([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?'
/home/runner/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhirpathpy/engine/nodes.py:8: SyntaxWarning: "\+" is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\+"? A raw string is also an option.
  dateTimeRE = '%s(T%s(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?' % (dateFormat, timeRE)
https://hapi.fhir.org/baseR4/Patient?_count=10&identifier=https://github.com/synthetichealth/synthea|
Query (Patient):   0%|          | 0/1 [00:00<?, ?it/s]Query (Patient): 100%|██████████| 1/1 [00:00<00:00, 839.36it/s]
resourceType id meta_versionId meta_lastUpdated meta_source text_status text_div identifier_0_system identifier_0_value identifier_1_type_coding_0_system ... address_0_country maritalStatus_coding_0_system maritalStatus_coding_0_code maritalStatus_coding_0_display maritalStatus_text multipleBirthBoolean communication_0_language_coding_0_system communication_0_language_coding_0_code communication_0_language_coding_0_display communication_0_language_text
0 Patient 129c6ac7-8d06-89de-ad63-0204a93e76c3 3 2026-02-27T02:21:05.552+00:00 #yGzIftcT7wgU5qdC generated <div xmlns="http://www.w3.org/1999/xhtml">Gene... https://github.com/synthetichealth/synthea 129c6ac7-8d06-89de-ad63-0204a93e76c3 http://terminology.hl7.org/CodeSystem/v2-0203 ... US http://terminology.hl7.org/CodeSystem/v3-Marit... M Married Married False urn:ietf:bcp:47 en-US English (United States) English (United States)
1 Patient 3af3708d-41f1-cd80-f3dd-ec5ac76072bf 3 2026-02-27T02:21:06.566+00:00 #OIOM1y47CP2PUagn generated <div xmlns="http://www.w3.org/1999/xhtml">Gene... https://github.com/synthetichealth/synthea 3af3708d-41f1-cd80-f3dd-ec5ac76072bf http://terminology.hl7.org/CodeSystem/v2-0203 ... US http://terminology.hl7.org/CodeSystem/v3-Marit... S Never Married Never Married False urn:ietf:bcp:47 en-US English (United States) English (United States)
2 Patient 63ee2253-bdd5-da55-2ad2-b4984d0ad700 3 2026-02-27T02:21:07.566+00:00 #2z3gGFbHs7ClCSN6 generated <div xmlns="http://www.w3.org/1999/xhtml">Gene... https://github.com/synthetichealth/synthea 63ee2253-bdd5-da55-2ad2-b4984d0ad700 http://terminology.hl7.org/CodeSystem/v2-0203 ... US http://terminology.hl7.org/CodeSystem/v3-Marit... S Never Married Never Married False urn:ietf:bcp:47 en-US English (United States) English (United States)
3 Patient 6a4160eb-a793-2f86-2302-378626f46cce 3 2026-02-27T02:21:08.567+00:00 #xrsTzPdUPatLM0oW generated <div xmlns="http://www.w3.org/1999/xhtml">Gene... https://github.com/synthetichealth/synthea 6a4160eb-a793-2f86-2302-378626f46cce http://terminology.hl7.org/CodeSystem/v2-0203 ... US http://terminology.hl7.org/CodeSystem/v3-Marit... M Married Married False urn:ietf:bcp:47 en-US English (United States) English (United States)
4 Patient 79a66c97-6131-3213-f3c9-4606946ab056 3 2026-02-27T02:21:09.567+00:00 #cZvE9V0qqKGTmNkj generated <div xmlns="http://www.w3.org/1999/xhtml">Gene... https://github.com/synthetichealth/synthea 79a66c97-6131-3213-f3c9-4606946ab056 http://terminology.hl7.org/CodeSystem/v2-0203 ... US http://terminology.hl7.org/CodeSystem/v3-Marit... M Married Married False urn:ietf:bcp:47 en-US English (United States) English (United States)

5 rows × 68 columns

It is easier to see the contents of this DataFrame by printing out its first row vertically:

# Print the first row of the DataFrame vertically for easier reading.
pd.set_option("display.max_rows", 100)  # Show all rows
df.head(1).T
0
resourceType Patient
id 129c6ac7-8d06-89de-ad63-0204a93e76c3
meta_versionId 3
meta_lastUpdated 2026-02-27T02:21:05.552+00:00
meta_source #yGzIftcT7wgU5qdC
text_status generated
text_div <div xmlns="http://www.w3.org/1999/xhtml">Gene...
identifier_0_system https://github.com/synthetichealth/synthea
identifier_0_value 129c6ac7-8d06-89de-ad63-0204a93e76c3
identifier_1_type_coding_0_system http://terminology.hl7.org/CodeSystem/v2-0203
identifier_1_type_coding_0_code MR
identifier_1_type_coding_0_display Medical Record Number
identifier_1_type_text Medical Record Number
identifier_1_system http://hospital.smarthealthit.org
identifier_1_value 129c6ac7-8d06-89de-ad63-0204a93e76c3
identifier_2_type_coding_0_system http://terminology.hl7.org/CodeSystem/v2-0203
identifier_2_type_coding_0_code SS
identifier_2_type_coding_0_display Social Security Number
identifier_2_type_text Social Security Number
identifier_2_system http://hl7.org/fhir/sid/us-ssn
identifier_2_value 999-94-5397
identifier_3_type_coding_0_system http://terminology.hl7.org/CodeSystem/v2-0203
identifier_3_type_coding_0_code DL
identifier_3_type_coding_0_display Driver's license number
identifier_3_type_text Driver's license number
identifier_3_system urn:oid:2.16.840.1.113883.4.3.25
identifier_3_value S99940903
identifier_4_type_coding_0_system http://terminology.hl7.org/CodeSystem/v2-0203
identifier_4_type_coding_0_code PPN
identifier_4_type_coding_0_display Passport Number
identifier_4_type_text Passport Number
identifier_4_system http://standardhealthrecord.org/fhir/Structure...
identifier_4_value X53631011X
name_0_use official
name_0_family Medhurst46
name_0_given_0 Sumiko254
name_0_given_1 Larue605
name_0_prefix_0 Mrs.
name_1_use maiden
name_1_family Cummerata161
name_1_given_0 Sumiko254
name_1_given_1 Larue605
name_1_prefix_0 Mrs.
telecom_0_system phone
telecom_0_value 555-810-7203
telecom_0_use home
gender female
birthDate 1927-05-21
deceasedDateTime 1989-05-09T20:35:22-04:00
address_0_extension_0_url http://hl7.org/fhir/StructureDefinition/geoloc...
address_0_extension_0_extension_0_url latitude
address_0_extension_0_extension_0_valueDecimal 38.377967
address_0_extension_0_extension_1_url longitude
address_0_extension_0_extension_1_valueDecimal -96.170608
address_0_line_0 633 Abernathy Landing
address_0_city Emporia
address_0_state KS
address_0_postalCode 66801
address_0_country US
maritalStatus_coding_0_system http://terminology.hl7.org/CodeSystem/v3-Marit...
maritalStatus_coding_0_code M
maritalStatus_coding_0_display Married
maritalStatus_text Married
multipleBirthBoolean False
communication_0_language_coding_0_system urn:ietf:bcp:47
communication_0_language_coding_0_code en-US
communication_0_language_coding_0_display English (United States)
communication_0_language_text English (United States)

If you look at the output above, you can see FHIR-PYrate collapsed the hierarchical FHIR data structure into DataFrame columns. FHIR-PYrate does this by taking an element from the FHIR-formatted data like Patient.identifier[0].value and converting to an underscore-delimited column name like identifier_0_value. (Note that Patient.identifier has multiple values in the FHIR data, so there are multiple identifier_N_... columns in the DataFrame.)

3 Selecting specific columns

Usually not every single value from a FHIR instance is needed for analysis. There are two ways to get a more concise DataFrame:

  1. Use the approach above to load all elements into a DataFrame, remove the unneeded columns, and rename the remaining columns as needed. The process_function capability in FHIR-PYrate allows you to integrate this approach into the bundles_to_dataframe() method call.
  2. Use FHIRPath to select specific elements and map them onto column names.

The second approach is typically more concise. For example, to generate a DataFrame like this…

id gender date_of_birth marital_status

…you could use the following code:

# Instantiate and perform the FHIR search interaction in a single function call
df = search.steal_bundles_to_dataframe(
    resource_type="Patient",
    request_params={
        "_count": 10,  # Get 10 instances per page
        "identifier": "https://github.com/synthetichealth/synthea|",
    },
    num_pages=1,  # Get 1 page (so a total of 10 instances)
    fhir_paths=[
        ("id", "identifier[0].value"),
        ("gender", "gender"),
        ("date_of_birth", "birthDate"),
        ("marital_status", "maritalStatus.coding[0].code"),
    ],
)
df
---------------------------------------------------------------------------
OptionalImportError                       Traceback (most recent call last)
Cell In[3], line 2
      1 # Instantiate and perform the FHIR search interaction in a single function call
----> 2 df = search.steal_bundles_to_dataframe(
      3     resource_type="Patient",
      4     request_params={
      5         "_count": 10,  # Get 10 instances per page
      6         "identifier": "https://github.com/synthetichealth/synthea|",
      7     },
      8     num_pages=1,  # Get 1 page (so a total of 10 instances)
      9     fhir_paths=[
     10         ("id", "identifier[0].value"),
     11         ("gender", "gender"),
     12         ("date_of_birth", "birthDate"),
     13         ("marital_status", "maritalStatus.coding[0].code"),
     14     ],
     15 )
     16 df

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/pirate.py:282, in Pirate.steal_bundles_to_dataframe(self, resource_type, request_params, num_pages, process_function, fhir_paths, build_df_after_query)
    251 def steal_bundles_to_dataframe(
    252     self,
    253     resource_type: str,
   (...)    258     build_df_after_query: bool = False,
    259 ) -> Union[pd.DataFrame, Dict[str, pd.DataFrame]]:
    260     """
    261     Execute a request, iterates through the result pages, and builds a DataFrame with their
    262     information. The DataFrames are either built after each
   (...)    280     returned.
    281     """
--> 282     return self._query_to_dataframe(self._get_bundles)(
    283         resource_type=resource_type,
    284         request_params=request_params,
    285         num_pages=num_pages,
    286         silence_tqdm=False,
    287         process_function=process_function,
    288         fhir_paths=fhir_paths,
    289         build_df_after_query=build_df_after_query,
    290         disable_multiprocessing_build=True,
    291         always_return_dict=False,
    292     )

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/pirate.py:1441, in Pirate._query_to_dataframe.<locals>.wrap(process_function, fhir_paths, build_df_after_query, disable_multiprocessing_build, always_return_dict, *args, **kwargs)
   1436 if fhir_paths is not None:
   1437     logger.info(
   1438         f"The selected process_function {process_function.__name__} will be "
   1439         f"overwritten."
   1440     )
-> 1441     process_function = self._set_up_fhirpath_function(fhir_paths)
   1442 return self._bundles_to_dataframe(
   1443     bundles=bundles_function(
   1444         *args, **kwargs, tqdm_df_build=not build_df_after_query
   (...)   1449     always_return_dict=always_return_dict,
   1450 )

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/pirate.py:1349, in Pirate._set_up_fhirpath_function(self, fhir_paths)
   1331             if (
   1332                 re.search(pattern=rf"{token}[\.\[]|[\.\]]{token}$", string=path)
   1333                 is not None
   1334             ):
   1335                 warnings.warn(
   1336                     f"You are using the term {token} in of your FHIR path {path}. "
   1337                     f"Please keep in mind that this token can be used a function according "
   (...)   1346                     stacklevel=2,
   1347                 )
   1348 compiled_paths = [
-> 1349     (name, fhirpathpy.compile(path=path)) for name, path in fhir_paths_with_name
   1350 ]
   1351 return partial(parse_fhir_path, compiled_fhir_paths=compiled_paths)

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/util/imports.py:106, in optional_import.<locals>._LazyRaise.__getattr__(self, name)
    101 def __getattr__(self, name: str) -> str:
    102     """
    103     Raise:
    104         OptionalImportError: When you call this method.
    105     """
--> 106     raise self._exception

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/util/imports.py:59, in optional_import(module, name, descriptor, allow_namespace_pkg)
     57     actual_cmd = f"import {module}"
     58 try:
---> 59     the_module = import_module(module)
     60     if not allow_namespace_pkg:
     61         is_namespace = getattr(the_module, "__file__", None) is None and hasattr(
     62             the_module, "__path__"
     63         )

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/importlib/__init__.py:88, in import_module(name, package)
     86             break
     87         level += 1
---> 88 return _bootstrap._gcd_import(name[level:], package, level)

File <frozen importlib._bootstrap>:1398, in _gcd_import(name, package, level)
   1396 if level > 0:
   1397     name = _resolve_name(name, package, level)
-> 1398 return _find_and_load(name, _gcd_import)

File <frozen importlib._bootstrap>:1371, in _find_and_load(name, import_)
   1369     module = sys.modules.get(name, _NEEDS_LOADING)
   1370     if module is _NEEDS_LOADING:
-> 1371         return _find_and_load_unlocked(name, import_)
   1373 # Optimization: only call _bootstrap._lock_unlock_module() if
   1374 # module.__spec__._initializing is True.
   1375 # NOTE: because of this, initializing must be set *before*
   1376 # putting the new module in sys.modules.
   1377 _lock_unlock_module(name)

File <frozen importlib._bootstrap>:1342, in _find_and_load_unlocked(name, import_)
   1340     parent_spec._uninitialized_submodules.append(child)
   1341 try:
-> 1342     module = _load_unlocked(spec)
   1343 finally:
   1344     if parent_spec:

File <frozen importlib._bootstrap>:938, in _load_unlocked(spec)
    935             raise ImportError('missing loader', name=spec.name)
    936         # A namespace package so do nothing.
    937     else:
--> 938         spec.loader.exec_module(module)
    939 except:
    940     try:

File <frozen importlib._bootstrap_external>:759, in _LoaderBasics.exec_module(self, module)
    756 if code is None:
    757     raise ImportError(f'cannot load module {module.__name__!r} when '
    758                       'get_code() returns None')
--> 759 _bootstrap._call_with_frames_removed(exec, code, module.__dict__)

File <frozen importlib._bootstrap>:491, in _call_with_frames_removed(f, *args, **kwds)
    483 def _call_with_frames_removed(f, *args, **kwds):
    484     """remove_importlib_frames in import.c will always remove sequences
    485     of importlib frames that end with a call to this function
    486 
   (...)    489     module code)
    490     """
--> 491     return f(*args, **kwds)

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhirpathpy/__init__.py:2
      1 from fhirpathpy.engine.invocations.constants import constants
----> 2 from fhirpathpy.parser import parse
      3 from fhirpathpy.engine import do_eval
      4 from fhirpathpy.engine.util import arraify, get_data, set_paths

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhirpathpy/parser/__init__.py:2
      1 import sys
----> 2 from antlr4 import *
      3 from antlr4.tree.Tree import ParseTreeWalker
      4 from antlr4.error.ErrorListener import ErrorListener

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/antlr4/__init__.py:6
      4 from antlr4.StdinStream import StdinStream
      5 from antlr4.BufferedTokenStream import TokenStream
----> 6 from antlr4.CommonTokenStream import CommonTokenStream
      7 from antlr4.Lexer import Lexer
      8 from antlr4.Parser import Parser

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/antlr4/CommonTokenStream.py:33
      1 #
      2 # Copyright (c) 2012-2017 The ANTLR Project. All rights reserved.
      3 # Use of this file is governed by the BSD 3-clause license that
   (...)     29 # channel.</p>
     30 #/
     32 from antlr4.BufferedTokenStream import BufferedTokenStream
---> 33 from antlr4.Lexer import Lexer
     34 from antlr4.Token import Token
     37 class CommonTokenStream(BufferedTokenStream):

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/antlr4/Lexer.py:12
      1 # Copyright (c) 2012-2017 The ANTLR Project. All rights reserved.
      2 # Use of this file is governed by the BSD 3-clause license that
      3 # can be found in the LICENSE.txt file in the project root.
   (...)      9 #  of speed.
     10 #/
     11 from io import StringIO
---> 12 from typing.io import TextIO
     13 import sys
     14 from antlr4.CommonTokenFactory import CommonTokenFactory

OptionalImportError: import fhirpathpy (No module named 'typing.io'; 'typing' is not a package).

While FHIRPath can be quite complex, its use in FHIR-PYrate is often straight forward. Nested elements are separated with ., and elements with multiple sub-values are identified by [N] where N is an integer starting at 0. The element paths can typically be constructed by loading all elements into a DataFrame and then manually deriving the FHIRPaths from the column names, or by looking at the hierarchy resource pages in the FHIR specification (see Key FHIR Resources for more information on reading the FHIR specification).

4 Elements with multiple sub-values

There are multiple identifier[N].value values for each instance of Patient in this dataset.

# Instantiate and perform the FHIR search interaction in a single function call
df = search.steal_bundles_to_dataframe(
    resource_type="Patient",
    request_params={
        "_count": 10,  # Get 10 instances per page
        "identifier": "https://github.com/synthetichealth/synthea|",
    },
    num_pages=1,  # Get 1 page (so a total of 10 instances)
    fhir_paths=[("id", "identifier[0].value"), ("identifiers", "identifier.value")],
)
df
---------------------------------------------------------------------------
OptionalImportError                       Traceback (most recent call last)
Cell In[4], line 2
      1 # Instantiate and perform the FHIR search interaction in a single function call
----> 2 df = search.steal_bundles_to_dataframe(
      3     resource_type="Patient",
      4     request_params={
      5         "_count": 10,  # Get 10 instances per page
      6         "identifier": "https://github.com/synthetichealth/synthea|",
      7     },
      8     num_pages=1,  # Get 1 page (so a total of 10 instances)
      9     fhir_paths=[("id", "identifier[0].value"), ("identifiers", "identifier.value")],
     10 )
     11 df

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/pirate.py:282, in Pirate.steal_bundles_to_dataframe(self, resource_type, request_params, num_pages, process_function, fhir_paths, build_df_after_query)
    251 def steal_bundles_to_dataframe(
    252     self,
    253     resource_type: str,
   (...)    258     build_df_after_query: bool = False,
    259 ) -> Union[pd.DataFrame, Dict[str, pd.DataFrame]]:
    260     """
    261     Execute a request, iterates through the result pages, and builds a DataFrame with their
    262     information. The DataFrames are either built after each
   (...)    280     returned.
    281     """
--> 282     return self._query_to_dataframe(self._get_bundles)(
    283         resource_type=resource_type,
    284         request_params=request_params,
    285         num_pages=num_pages,
    286         silence_tqdm=False,
    287         process_function=process_function,
    288         fhir_paths=fhir_paths,
    289         build_df_after_query=build_df_after_query,
    290         disable_multiprocessing_build=True,
    291         always_return_dict=False,
    292     )

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/pirate.py:1441, in Pirate._query_to_dataframe.<locals>.wrap(process_function, fhir_paths, build_df_after_query, disable_multiprocessing_build, always_return_dict, *args, **kwargs)
   1436 if fhir_paths is not None:
   1437     logger.info(
   1438         f"The selected process_function {process_function.__name__} will be "
   1439         f"overwritten."
   1440     )
-> 1441     process_function = self._set_up_fhirpath_function(fhir_paths)
   1442 return self._bundles_to_dataframe(
   1443     bundles=bundles_function(
   1444         *args, **kwargs, tqdm_df_build=not build_df_after_query
   (...)   1449     always_return_dict=always_return_dict,
   1450 )

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/pirate.py:1349, in Pirate._set_up_fhirpath_function(self, fhir_paths)
   1331             if (
   1332                 re.search(pattern=rf"{token}[\.\[]|[\.\]]{token}$", string=path)
   1333                 is not None
   1334             ):
   1335                 warnings.warn(
   1336                     f"You are using the term {token} in of your FHIR path {path}. "
   1337                     f"Please keep in mind that this token can be used a function according "
   (...)   1346                     stacklevel=2,
   1347                 )
   1348 compiled_paths = [
-> 1349     (name, fhirpathpy.compile(path=path)) for name, path in fhir_paths_with_name
   1350 ]
   1351 return partial(parse_fhir_path, compiled_fhir_paths=compiled_paths)

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/util/imports.py:106, in optional_import.<locals>._LazyRaise.__getattr__(self, name)
    101 def __getattr__(self, name: str) -> str:
    102     """
    103     Raise:
    104         OptionalImportError: When you call this method.
    105     """
--> 106     raise self._exception

    [... skipping hidden 1 frame]

Cell In[3], line 2
      1 # Instantiate and perform the FHIR search interaction in a single function call
----> 2 df = search.steal_bundles_to_dataframe(
      3     resource_type="Patient",
      4     request_params={
      5         "_count": 10,  # Get 10 instances per page
      6         "identifier": "https://github.com/synthetichealth/synthea|",
      7     },
      8     num_pages=1,  # Get 1 page (so a total of 10 instances)
      9     fhir_paths=[
     10         ("id", "identifier[0].value"),
     11         ("gender", "gender"),
     12         ("date_of_birth", "birthDate"),
     13         ("marital_status", "maritalStatus.coding[0].code"),
     14     ],
     15 )
     16 df

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/pirate.py:282, in Pirate.steal_bundles_to_dataframe(self, resource_type, request_params, num_pages, process_function, fhir_paths, build_df_after_query)
    251 def steal_bundles_to_dataframe(
    252     self,
    253     resource_type: str,
   (...)    258     build_df_after_query: bool = False,
    259 ) -> Union[pd.DataFrame, Dict[str, pd.DataFrame]]:
    260     """
    261     Execute a request, iterates through the result pages, and builds a DataFrame with their
    262     information. The DataFrames are either built after each
   (...)    280     returned.
    281     """
--> 282     return self._query_to_dataframe(self._get_bundles)(
    283         resource_type=resource_type,
    284         request_params=request_params,
    285         num_pages=num_pages,
    286         silence_tqdm=False,
    287         process_function=process_function,
    288         fhir_paths=fhir_paths,
    289         build_df_after_query=build_df_after_query,
    290         disable_multiprocessing_build=True,
    291         always_return_dict=False,
    292     )

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/pirate.py:1441, in Pirate._query_to_dataframe.<locals>.wrap(process_function, fhir_paths, build_df_after_query, disable_multiprocessing_build, always_return_dict, *args, **kwargs)
   1436 if fhir_paths is not None:
   1437     logger.info(
   1438         f"The selected process_function {process_function.__name__} will be "
   1439         f"overwritten."
   1440     )
-> 1441     process_function = self._set_up_fhirpath_function(fhir_paths)
   1442 return self._bundles_to_dataframe(
   1443     bundles=bundles_function(
   1444         *args, **kwargs, tqdm_df_build=not build_df_after_query
   (...)   1449     always_return_dict=always_return_dict,
   1450 )

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/pirate.py:1349, in Pirate._set_up_fhirpath_function(self, fhir_paths)
   1331             if (
   1332                 re.search(pattern=rf"{token}[\.\[]|[\.\]]{token}$", string=path)
   1333                 is not None
   1334             ):
   1335                 warnings.warn(
   1336                     f"You are using the term {token} in of your FHIR path {path}. "
   1337                     f"Please keep in mind that this token can be used a function according "
   (...)   1346                     stacklevel=2,
   1347                 )
   1348 compiled_paths = [
-> 1349     (name, fhirpathpy.compile(path=path)) for name, path in fhir_paths_with_name
   1350 ]
   1351 return partial(parse_fhir_path, compiled_fhir_paths=compiled_paths)

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/util/imports.py:106, in optional_import.<locals>._LazyRaise.__getattr__(self, name)
    101 def __getattr__(self, name: str) -> str:
    102     """
    103     Raise:
    104         OptionalImportError: When you call this method.
    105     """
--> 106     raise self._exception

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhir_pyrate/util/imports.py:59, in optional_import(module, name, descriptor, allow_namespace_pkg)
     57     actual_cmd = f"import {module}"
     58 try:
---> 59     the_module = import_module(module)
     60     if not allow_namespace_pkg:
     61         is_namespace = getattr(the_module, "__file__", None) is None and hasattr(
     62             the_module, "__path__"
     63         )

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/importlib/__init__.py:88, in import_module(name, package)
     86             break
     87         level += 1
---> 88 return _bootstrap._gcd_import(name[level:], package, level)

File <frozen importlib._bootstrap>:1398, in _gcd_import(name, package, level)
   1396 if level > 0:
   1397     name = _resolve_name(name, package, level)
-> 1398 return _find_and_load(name, _gcd_import)

File <frozen importlib._bootstrap>:1371, in _find_and_load(name, import_)
   1369     module = sys.modules.get(name, _NEEDS_LOADING)
   1370     if module is _NEEDS_LOADING:
-> 1371         return _find_and_load_unlocked(name, import_)
   1373 # Optimization: only call _bootstrap._lock_unlock_module() if
   1374 # module.__spec__._initializing is True.
   1375 # NOTE: because of this, initializing must be set *before*
   1376 # putting the new module in sys.modules.
   1377 _lock_unlock_module(name)

File <frozen importlib._bootstrap>:1342, in _find_and_load_unlocked(name, import_)
   1340     parent_spec._uninitialized_submodules.append(child)
   1341 try:
-> 1342     module = _load_unlocked(spec)
   1343 finally:
   1344     if parent_spec:

File <frozen importlib._bootstrap>:938, in _load_unlocked(spec)
    935             raise ImportError('missing loader', name=spec.name)
    936         # A namespace package so do nothing.
    937     else:
--> 938         spec.loader.exec_module(module)
    939 except:
    940     try:

File <frozen importlib._bootstrap_external>:759, in _LoaderBasics.exec_module(self, module)
    756 if code is None:
    757     raise ImportError(f'cannot load module {module.__name__!r} when '
    758                       'get_code() returns None')
--> 759 _bootstrap._call_with_frames_removed(exec, code, module.__dict__)

File <frozen importlib._bootstrap>:491, in _call_with_frames_removed(f, *args, **kwds)
    483 def _call_with_frames_removed(f, *args, **kwds):
    484     """remove_importlib_frames in import.c will always remove sequences
    485     of importlib frames that end with a call to this function
    486 
   (...)    489     module code)
    490     """
--> 491     return f(*args, **kwds)

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhirpathpy/__init__.py:2
      1 from fhirpathpy.engine.invocations.constants import constants
----> 2 from fhirpathpy.parser import parse
      3 from fhirpathpy.engine import do_eval
      4 from fhirpathpy.engine.util import arraify, get_data, set_paths

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/fhirpathpy/parser/__init__.py:2
      1 import sys
----> 2 from antlr4 import *
      3 from antlr4.tree.Tree import ParseTreeWalker
      4 from antlr4.error.ErrorListener import ErrorListener

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/antlr4/__init__.py:6
      4 from antlr4.StdinStream import StdinStream
      5 from antlr4.BufferedTokenStream import TokenStream
----> 6 from antlr4.CommonTokenStream import CommonTokenStream
      7 from antlr4.Lexer import Lexer
      8 from antlr4.Parser import Parser

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/antlr4/CommonTokenStream.py:33
      1 #
      2 # Copyright (c) 2012-2017 The ANTLR Project. All rights reserved.
      3 # Use of this file is governed by the BSD 3-clause license that
   (...)     29 # channel.</p>
     30 #/
     32 from antlr4.BufferedTokenStream import BufferedTokenStream
---> 33 from antlr4.Lexer import Lexer
     34 from antlr4.Token import Token
     37 class CommonTokenStream(BufferedTokenStream):

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/antlr4/Lexer.py:12
      1 # Copyright (c) 2012-2017 The ANTLR Project. All rights reserved.
      2 # Use of this file is governed by the BSD 3-clause license that
      3 # can be found in the LICENSE.txt file in the project root.
   (...)      9 #  of speed.
     10 #/
     11 from io import StringIO
---> 12 from typing.io import TextIO
     13 import sys
     14 from antlr4.CommonTokenFactory import CommonTokenFactory

OptionalImportError: import fhirpathpy (No module named 'typing.io'; 'typing' is not a package).

To convert to separate columns, you can do the following:

df.join(pd.DataFrame(df.pop("identifiers").values.tolist()).add_prefix("identifier_"))
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/pandas/core/indexes/base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7088, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7096, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'identifiers'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[5], line 1
----> 1 df.join(pd.DataFrame(df.pop("identifiers").values.tolist()).add_prefix("identifier_"))

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/pandas/core/frame.py:5841, in DataFrame.pop(self, item)
   5800 def pop(self, item: Hashable) -> Series:
   5801     """
   5802     Return item and drop from frame. Raise KeyError if not found.
   5803 
   (...)   5839     3  monkey        NaN
   5840     """
-> 5841     return super().pop(item=item)

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/pandas/core/generic.py:950, in NDFrame.pop(self, item)
    949 def pop(self, item: Hashable) -> Series | Any:
--> 950     result = self[item]
    951     del self[item]
    953     return result

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/pandas/core/frame.py:4113, in DataFrame.__getitem__(self, key)
   4111 if self.columns.nlevels > 1:
   4112     return self._getitem_multilevel(key)
-> 4113 indexer = self.columns.get_loc(key)
   4114 if is_integer(indexer):
   4115     indexer = [indexer]

File ~/work/fhir-for-research/fhir-for-research/.venv/lib/python3.14/site-packages/pandas/core/indexes/base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 'identifiers'

This will give you separate identifier_0, identifier_1, … columns for each Patient.identifier[N] value.