PBIXRay Python Library
pbixray is an open source Python library for parsing Power BI .pbix files directly from Python, without Power BI Desktop or a live Analysis Services connection.
It is designed for developers who need to inspect semantic models, extract metadata, read Power Query logic, and work with the internals of PBIX files programmatically. The same API also supports Excel .xlsx files that contain embedded PowerPivot models and Analysis Services backup (.abf) files.
pbixray is read-only: it returns pandas DataFrames and requires no network access and no Power BI or Excel install. File type (PBIX vs XLSX vs ABF) is auto-detected from the file contents — the same API works either way.
Install
pip install pbixray
Quick Start
from pbixray import PBIXRay
model = PBIXRay("path/to/your_report.pbix")
print(model.tables)
print(model.metadata)
print(model.power_query)
print(model.dax_measures)
print(model.relationships)
Supported Inputs
- Power BI
.pbixfiles - Excel
.xlsxfiles with embedded PowerPivot models - Analysis Services backup (
.abf) files — same data model as a.pbix, just without the zip envelope, soPBIXRay("path/to/backup.abf")works unchanged
Multi-partition tables — both classic SSAS partitioning and incremental-refresh partitions — are decoded in full: get_table() concatenates every partition in storage order.
Large Models (on-disk loading)
By default the entire decompressed data model is held in memory. For models whose uncompressed size approaches or exceeds available RAM, pass on_disk=True: the decompressed data is streamed to a temporary file and memory-mapped, so only the pages a requested table actually touches are faulted in. Use temp_dir to control where the spill file is created (it defaults to the system temp directory).
# Spill to disk + mmap instead of holding everything in RAM.
with PBIXRay("path/to/large.pbix", on_disk=True, temp_dir="/fast/scratch") as model:
df = model.get_table("Sales")
# leaving the `with` block releases the mapping and removes the temp file
PBIXRay is also a context manager. Calling model.close() (or exiting the with block) deterministically releases the memory map and the metadata connection. When on_disk=False (the default) behavior is unchanged. Metadata (DAX, tmschema_*, etc.) is loaded lazily on first access, so simply opening a file is cheap.
The DataModel member is read in place from the container file whenever it is STORED in the zip (the normal case — it carries its own compression). If the member is additionally uncompressed (a raw ABF backup inside the zip), on_disk=True serves it directly from the .pbix/.xlsx with no temp-file copy at all.
Core Properties
These properties expose the most useful parts of a model as Python values or pandas DataFrames.
| Property | Returns |
|---|---|
model.tables | List of table names in the model |
model.metadata | Metadata about the Power BI configuration used to create the model |
model.power_query | DataFrame of Power Query / M expressions with TableName and Expression |
model.m_parameters | DataFrame of M parameters with ParameterName, Description, Expression, and ModifiedTime |
model.size | Model size in bytes (int) |
model.dax_tables | DataFrame of calculated tables with TableName and Expression |
model.dax_measures | DataFrame of measures with TableName, Name, Expression, DisplayFolder, and Description |
model.dax_columns | DataFrame of calculated columns with TableName, ColumnName, and Expression |
model.aggregations | DataFrame of resolved aggregation mappings (“Manage aggregations”) with AggregationTable, AggregationColumn, Summarization, DetailTable, and DetailColumn |
model.schema | DataFrame of schema info with TableName, ColumnName, and PandasDataType |
model.relationships | DataFrame of relationships with FromTableName, FromColumnName, ToTableName, ToColumnName, IsActive, Cardinality, CrossFilteringBehavior, FromKeyCount, ToKeyCount, and RelyOnReferentialIntegrity |
model.rls | DataFrame of row-level security with TableName, RoleName, RoleDescription, FilterExpression, State, and MetadataPermission |
model.ols | DataFrame of object-level security with RoleName, TableName, ColumnName, Scope, and Permission |
model.perspectives | DataFrame of perspective membership with PerspectiveName, ObjectType, TableName, ObjectName, and IncludeAll |
model.statistics | DataFrame of column statistics with TableName, ColumnName, Cardinality, Dictionary, HashIndex, and DataSize |
model.connections | List of dictionaries describing the report’s Connections manifest (empty for self-contained import models) |
model.data_mashup | Parsed DataMashup part ([MS-QDEFF]), or None when the file has no mashup |
model.mashup_queries | DataFrame of DataMashup queries/parameters with Name, Kind, IsParameter, Expression, Type, DefaultValue, and AllowedValues |
Common Examples
List tables
print(model.tables)
Read Power Query / M code
power_query = model.power_query
print(power_query[["TableName", "Expression"]])
Inspect measures
measures = model.dax_measures
print(measures[["TableName", "Name", "Expression"]])
Inspect calculated columns
columns = model.dax_columns
print(columns[["TableName", "ColumnName", "Expression"]])
Inspect relationships
relationships = model.relationships
print(
relationships[
[
"FromTableName",
"FromColumnName",
"ToTableName",
"ToColumnName",
"Cardinality",
"IsActive",
]
]
)
Inspect row-level security
rls = model.rls
print(rls[["RoleName", "TableName", "FilterExpression"]])
Inspect object-level security
ols = model.ols
print(ols[["RoleName", "TableName", "ColumnName", "Scope", "Permission"]])
Each row is one secured object: Scope='Column' rows hide or expose a single column, Scope='Table' rows (where ColumnName is None) a whole table. Permission is None (hidden), Read (visible) or Default. Plain row-level-security rows are excluded — see model.rls.
Inspect perspectives
perspectives = model.perspectives
print(perspectives[["PerspectiveName", "ObjectType", "TableName", "ObjectName"]])
Each row is one object included in a perspective; ObjectType is Table, Column, Measure or Hierarchy, and IncludeAll is populated only for Table rows. This is a friendly roll-up over the raw tmschema_perspective_* endpoints.
Inspect aggregations
aggregations = model.aggregations
print(aggregations)
Each row maps one aggregation-table column (“Manage aggregations”) to a detail (base) table. Summarization is the human label (GroupBy, Sum, Count, Min, Max); DetailColumn is None for the “Count table rows” case.
All three return an empty DataFrame (with the documented columns) on models that don’t use the feature.
Read a table’s contents
sales = model.get_table("Sales")
print(sales.head())
To decode only a subset of columns from a wide table (decoding the others is skipped), pass columns:
sales = model.get_table("Sales", columns=["ProductKey", "Sales"])
With strings_as_categorical=True string columns come back as pd.Categorical, so each distinct value is stored once instead of once per row — a large memory saving on low-cardinality string columns:
sales = model.get_table("Sales", strings_as_categorical=True)
Dictionary decode runs on a native Huffman kernel (xmhuffman) and fans out across cores automatically for large dictionaries.
Stream large tables in chunks
For tables too large to materialize whole, iter_table yields DataFrame chunks instead of one DataFrame. Chunks follow VertiPaq segment boundaries, and chunk_size further splits each segment (chunks never span two segments, so tail chunks may be shorter). String columns default to pd.Categorical, sharing one categories array across all chunks; pass strings_as_categorical=False for plain object-dtype strings.
with PBIXRay("path/to/large.pbix", on_disk=True) as model:
for chunk in model.iter_table("Sales", chunk_size=1_000_000):
process(chunk) # chunk.index is the global row range
The dictionaries of every selected column are decoded up front and kept for the whole iteration, so on dictionary-heavy models (e.g. wide free-text columns) pass columns to project only what you need. Combine with on_disk=True to also keep the decompressed model itself out of RAM.
Inspect data connections
print(model.connections)
Reports expose their Connections manifest — the list of data connections declared by the report — as a list of dictionaries. Self-contained (import) models usually return an empty list.
Power Query from DataMashup
power_query and m_parameters read the M from the Analysis Services metadata, which works for import models. Some models — notably DirectQuery / native SQL — keep their queries and parameters only in the report’s DataMashup part ([MS-QDEFF]). model.data_mashup and model.mashup_queries parse that part directly:
df = model.mashup_queries # Name, Kind, IsParameter, Expression, Type, DefaultValue, AllowedValues
params = df[df["IsParameter"]] # the Power Query parameters and their metadata
mashup = model.data_mashup # None when the file has no DataMashup part
if mashup is not None:
print(mashup.version)
for q in mashup.parameters: # MQuery objects
print(q.name, q.param_type, q.default_value, q.allowed_values)
data_mashup is None for files without a mashup, and raises DataMashupError if the part is malformed. These accessors are additive — power_query and m_parameters keep their existing AS-metadata behavior.
Live-Connection (Thin) Reports
Some .pbix files are thin reports with no embedded model: they live-connect to an external Analysis Services server (analysisServicesDatabaseLive) or a Power BI Service dataset (pbiServiceLive). Because the model lives on a remote server, there is nothing to extract on disk, and constructing PBIXRay raises LiveConnectionError. The exception carries the parsed connection details so you can still identify what the report points at:
from pbixray import PBIXRay, LiveConnectionError, NoEmbeddedModelError
try:
model = PBIXRay("thin-report.pbix")
except LiveConnectionError as e:
print(e.connection_type) # e.g. 'pbiServiceLive'
print(e.database_name) # remote dataset id, when available
print(e.connections) # full manifest (list of dicts)
The exception hierarchy is LiveConnectionError → NoEmbeddedModelError → PBIXRayError. NoEmbeddedModelError is raised when a file has no model and no connection manifest. Both also subclass RuntimeError for backward compatibility.
Data Model Details
pbixray can be used both as a quick inspection tool and as a lower-level metadata extraction library.
Use it to:
- enumerate tables and columns in a semantic model
- inspect DAX calculated tables, measures, and calculated columns
- extract Power Query and M parameter definitions
- audit relationships, referential settings, and cross-filtering behavior
- review row-level and object-level security rules
- resolve aggregation mappings and perspective membership
- measure model size and storage statistics
Tabular Model Schema Endpoints
pbixray also exposes direct equivalents of all 40 Analysis Services $System.TMSCHEMA_* DMVs by reading the embedded SQLite metadata database inside the PBIX. These endpoints are PBIX-only — on XLSX files they return empty DataFrames.
| Property | DMV equivalent |
|---|---|
model.tmschema_model | TMSCHEMA_MODEL |
model.tmschema_tables | TMSCHEMA_TABLES |
model.tmschema_columns | TMSCHEMA_COLUMNS |
model.tmschema_partitions | TMSCHEMA_PARTITIONS |
model.tmschema_hierarchies | TMSCHEMA_HIERARCHIES |
model.tmschema_levels | TMSCHEMA_LEVELS |
model.tmschema_datasources | TMSCHEMA_DATASOURCES |
model.tmschema_perspectives | TMSCHEMA_PERSPECTIVES |
model.tmschema_perspective_tables | TMSCHEMA_PERSPECTIVE_TABLES |
model.tmschema_perspective_columns | TMSCHEMA_PERSPECTIVE_COLUMNS |
model.tmschema_perspective_hierarchies | TMSCHEMA_PERSPECTIVE_HIERARCHIES |
model.tmschema_perspective_measures | TMSCHEMA_PERSPECTIVE_MEASURES |
model.tmschema_kpis | TMSCHEMA_KPIS |
model.tmschema_annotations | TMSCHEMA_ANNOTATIONS |
model.tmschema_extended_properties | TMSCHEMA_EXTENDED_PROPERTIES |
model.tmschema_cultures | TMSCHEMA_CULTURES |
model.tmschema_translations | TMSCHEMA_OBJECT_TRANSLATIONS |
model.tmschema_linguistic_metadata | TMSCHEMA_LINGUISTIC_METADATA |
model.tmschema_query_groups | TMSCHEMA_QUERY_GROUPS |
model.tmschema_calculation_groups | TMSCHEMA_CALCULATION_GROUPS |
model.tmschema_calculation_items | TMSCHEMA_CALCULATION_ITEMS |
model.tmschema_calculation_expressions | TMSCHEMA_CALCULATION_EXPRESSIONS |
model.tmschema_variations | TMSCHEMA_VARIATIONS |
model.tmschema_attribute_hierarchies | TMSCHEMA_ATTRIBUTE_HIERARCHIES |
model.tmschema_sets | TMSCHEMA_SETS |
model.tmschema_refresh_policies | TMSCHEMA_REFRESH_POLICIES |
model.tmschema_detail_rows_definitions | TMSCHEMA_DETAIL_ROWS_DEFINITIONS |
model.tmschema_format_string_definitions | TMSCHEMA_FORMAT_STRING_DEFINITIONS |
model.tmschema_functions | TMSCHEMA_FUNCTIONS |
model.tmschema_calendars | TMSCHEMA_CALENDARS |
model.tmschema_calendar_column_groups | TMSCHEMA_CALENDAR_COLUMN_GROUPS |
model.tmschema_calendar_column_refs | TMSCHEMA_CALENDAR_COLUMN_REFERENCES |
model.tmschema_alternate_of | TMSCHEMA_ALTERNATE_OF |
model.tmschema_related_column_details | TMSCHEMA_RELATED_COLUMN_DETAILS |
model.tmschema_group_by_columns | TMSCHEMA_GROUP_BY_COLUMNS |
model.tmschema_binding_info | TMSCHEMA_BINDING_INFO |
model.tmschema_analytics_ai_metadata | TMSCHEMA_ANALYTICS_AI_METADATA |
model.tmschema_data_coverage_definitions | TMSCHEMA_DATA_COVERAGE_DEFINITIONS |
model.tmschema_role_memberships | TMSCHEMA_ROLE_MEMBERSHIPS |
model.tmschema_column_permissions | TMSCHEMA_COLUMN_PERMISSIONS |
TMSCHEMA examples
# List all columns with table names and hidden flags
print(model.tmschema_columns[["TableName", "Name", "DataType", "IsHidden"]])
# Inspect incremental refresh policies
print(model.tmschema_refresh_policies)
# Inspect security role memberships
print(model.tmschema_role_memberships)
PBIX vs XLSX Capability Matrix
Both PBIX and XLSX (PowerPivot) files use the same API, but coverage differs. “Empty” below means a zero-row DataFrame — never None and never an exception.
| Endpoint | PBIX | XLSX |
|---|---|---|
tables, schema, statistics, size | Populated | Populated |
get_table(name) | Real data | Real data (no RowNumber) |
relationships | Populated | Populated |
dax_tables | Populated | Populated (from partitions) |
dax_measures | Populated | Populated (measure groups) |
dax_columns | Populated | Empty |
power_query, m_parameters | Populated | Empty |
metadata, rls | Populated | Empty |
aggregations, ols, perspectives | Populated | Empty |
tmschema_* (all 40) | Populated | Empty |
Notes and Gotchas
RowNumberis dropped fromget_table()output — it is a VertiPaq internal storage position, not user data.- Row order is storage order, not insertion order. VertiPaq sorts rows by lowest-cardinality columns first for RLE compression. Two calls are stable, but order will differ from a CSV exported from Excel. For row equivalence, compare as multisets:
df.sort_values(list(df.columns)).reset_index(drop=True). - Unknown table name → empty DataFrame, not an exception. Validate against
model.tablesif you need to detect bad names. model.tablesreturns a numpy array, not a Python list. It iterates fine, butmodel.tables == [...]won’t work as a plain equality check.- XLSX calculated columns can have a display name different from the internal storage name (e.g.
Category↔CalculatedColumn1).pbixrayresolves these soschema.ColumnNameandget_table()use the display name. get_table()materializes the whole table into a DataFrame, so large fact tables can be memory-heavy. Useiter_table()to stream in chunks, pluson_disk=True, thecolumnsargument, andstrings_as_categorical=Trueto keep memory in check.
Out of Scope
pbixray is a read-only data-model extractor. It does not:
- write, modify, or repack PBIX/XLSX files
- evaluate DAX — measure and calculated-column expressions are returned as source text only
- run a query engine (no DAX/MDX/M evaluation against the model)
- connect to Power BI Service, Analysis Services, datasets, gateways, or workspaces
- refresh the model
- parse the report layer (visuals, pages, bookmarks, themes) — data model only
- support
.pbit,.pbids, or.pbipformats — only.pbix,.xlsxwith an embedded PowerPivot model, and.abfbackups
Requirements
- Python 3.8+ (tested through 3.13)
- Runtime dependencies:
xpress8,xpress9,xmhuffman,kaitaistruct,numpy,pandas,apsw - No Power BI Desktop required
- Works on macOS, Linux, and Windows