If you’ve ever watched a team pour months into a semantic model—only to treat it as “the thing Power BI reads”—you’ve seen a common (and costly) mental model at work.
Semantic models shouldn’t be the last layer before visualization. In Microsoft Fabric, they can be a first-class part of the extended analytics and data science stack: something you can query, validate, profile, and even productize from notebooks. That shift is exactly what SemPy (the Python library behind Semantic Link) makes practical.
In this post, I’m going to do three things:
- Introduce SemPy for Fabric as the bridge between semantic models and the rest of your Python workflows.
- Share four “generic” functions that help you discover and understand a model’s surface area.
- Highlight three functions that make consuming semantic models straightforward, and three that unlock capabilities you’d otherwise spend real time (and compute) rebuilding yourself.
Along the way, I’ll frame these as patterns for turning semantic models into data product surface areas—usable well beyond dashboards. This is where Microsoft Fabric and #SemPy start to feel less like “BI tooling” and more like part of your day-to-day analytics engineering and Data Science workflow.
SemPy in one paragraph
Semantic Link is a Fabric feature that connects semantic models with data science capabilities in Fabric—specifically so you can work with those models from notebooks and optimize items for performance, memory, and cost.
SemPy is the Python layer you actually touch. In Fabric notebooks running Spark 3.4+, semantic link is available in the default runtime (and for older runtimes you can install/upgrade it with pip).
The part that tends to get missed: SemPy doesn’t just “pull data.” It pulls data plus semantic context via FabricDataFrame, which is designed to store and propagate Power BI metadata (dataset, table, column, data category, and more).
That propagation is the trick that turns a semantic model from a reporting endpoint into a reusable surface area—something you can treat like a product contract across #PowerBI and the rest of your stack.
A quick pattern that matters more than any single function
Before the function tour, here’s the pattern I see most successful teams converge on:
Semantic model as a product surface
A semantic model already contains:
- A curated schema (tables/columns)
- Business definitions (measures)
- A join graph (relationships)
- Security semantics (roles/RLS)
- Performance constraints (storage/memory footprint)
SemPy makes those assets consumable in code, which means the semantic model becomes an interface—like an internal data product API—rather than a terminal layer.
And SemPy is designed to “just work” in Fabric notebooks even when your notebook, lakehouse, and semantic model aren’t all in the same place. By default, SemPy resolves the workspace based on the attached lakehouse (if you have one) or the notebook workspace; if your model lives elsewhere, you specify workspace=.
Now, onto the underused functions.
Four generic SemPy functions that help you understand the model surface
These are the “get oriented fast” functions. They’re not glamorous, but they’re the difference between guessing and operating with a contract.
fabric.list_datasets()
This is how you stop hardcoding model names and start treating the semantic model as an discoverable artifact.
Two details make it more important than it looks:
- It supports
mode="xmla"(default) andmode="rest". - In XMLA mode it leverages TOM and requires at least ReadWrite permissions; REST mode is the alternative when you’re working with more limited access. (Microsoft Learn)
That makes list_datasets() a practical first line in any notebook you intend to operationalize.
import sempy.fabric as fabricmodels = fabric.list_datasets(workspace="Sales Analytics", mode="rest")models.head()
fabric.list_tables()
Semantic models are often “known” socially (“there’s a Sales table… somewhere”), but rarely known programmatically.
list_tables() gives you an inventory of tables and can optionally include column-level detail via include_columns=True. It uses TOM and therefore expects ReadWrite permissions on the model.
tables = fabric.list_tables( dataset="Sales Semantic Model", workspace="Sales Analytics", include_columns=True)tables.head()
This is also one of the cleanest ways to build lightweight documentation for a model’s contract, or to sanity-check what’s actually deployed versus what you think is deployed.
fabric.list_measures()
Measures are business logic, and business logic is often the most valuable thing in your semantic model.
list_measures() gives you a DataFrame of measures and their attributes. Like the other TOM-backed metadata functions, it requires ReadWrite permissions.
measures = fabric.list_measures("Sales Semantic Model", workspace="Sales Analytics")measures[["Name", "Expression"]].head()
If your goal is to make the semantic model usable beyond visuals, measures are the “feature definitions” you want to expose to notebooks and pipelines.
fabric.list_relationships()
Teams love star schemas—until something breaks and no one can prove the join paths are still valid.
list_relationships() returns the relationship graph and even supports calculating missing rows via calculate_missing_rows=True (handy when you suspect referential integrity issues in practice). It’s TOM-backed and requires ReadWrite permissions.
rels = fabric.list_relationships( "Sales Semantic Model", workspace="Sales Analytics", calculate_missing_rows=True)rels.head()
This is one of those functions you don’t appreciate until you’re debugging a metric drift issue and realize the “semantic layer” is the real integration layer.
Three functions that make semantic models easy to consume (not just inspect)
Once you know what the model exposes, these are the workhorses for actually using it in analysis and ML workflows.
fabric.read_table()
read_table() is the “give me the data” function, but it’s smarter than a typical extract.
It can read a table from a semantic model using different modes—including "xmla", "rest", and "onelake"—and returns a FabricDataFrame.
That matters because it lets you choose how you want to access the data (and what access paths you’re allowed to use) without rewriting the rest of your notebook.
sales = fabric.read_table( dataset="Sales Semantic Model", table="Sales", workspace="Sales Analytics", mode="onelake" # or "rest" / "xmla" depending on your scenario)sales.head()
fabric.evaluate_measure()
This is where semantic models start acting like product APIs.
evaluate_measure() computes one or more Power BI measures, optionally grouped by columns and filtered. It returns a FabricDataFrame, and it exposes a use_xmla switch to choose XMLA vs REST evaluation behavior.
If you’ve ever tried to recreate complex DAX logic in Python “just for a model,” you already know why this is a big deal.
features = fabric.evaluate_measure( dataset="Sales Semantic Model", measure=["Total Sales", "Gross Margin %"], groupby_columns=["Customer[Customer Key]"], workspace="Sales Analytics")features.head()
A nice framing: measures become reusable feature definitions for notebooks, forecasts, experimentation, and monitoring.
fabric.evaluate_dax()
When you need something more custom than read_table() or evaluate_measure(), evaluate_dax() runs an arbitrary DAX query and returns a FabricDataFrame.
Two underused capabilities here:
role=lets you impersonate a model role.effective_user_name=lets you impersonate an effective user (and you can’t use both at once).
That makes it an excellent tool for testing how your semantics behave under security contexts—without waiting for someone to notice in a report.
query = """EVALUATESUMMARIZECOLUMNS( 'Date'[Year], "Sales", [Total Sales])"""df = fabric.evaluate_dax( dataset="Sales Semantic Model", dax_string=query, workspace="Sales Analytics", role="Sales Rep")df.head()
Three SemPy functions that unlock “expensive” capabilities (the stuff you don’t want to rebuild)
This last group is where SemPy quietly earns its keep. These functions give you insight and governance leverage that is tedious—or computationally heavy—to reproduce from scratch.
fabric.get_model_calc_dependencies()
This function calculates dependencies for all objects in a semantic model.
In practice, that means impact analysis: “If I touch this measure, what breaks?” or “Which columns are driving this calculation tree?” Doing this reliably without SemPy means parsing DAX, chasing references, and accepting edge cases.
deps_iter = fabric.get_model_calc_dependencies( dataset="Sales Semantic Model", workspace="Sales Analytics")# It's an iterator; materialize if neededdeps = list(deps_iter)deps[0]
This is a data product capability: it’s how you stop treating measure edits as artisanal craft and start treating them as managed change.
fabric.model_memory_analyzer()
model_memory_analyzer() provides memory/storage statistics about objects in a semantic model (tables, columns, relationships, partitions, etc.).
That’s not just a “nice to know.” In Fabric, performance and cost are inseparable from model design. Memory hotspots are where refresh times balloon and where capacity pressure shows up first.
memory_stats = fabric.model_memory_analyzer( dataset="Sales Semantic Model", workspace="Sales Analytics")memory_stats.head()
fabric.run_model_bpa()
run_model_bpa() runs a Best Practice Analyzer over the semantic model.
Microsoft’s notebook experience highlights BPA as a way to get actionable tips across categories like performance, DAX expressions, error prevention, maintenance, and formatting—checking a substantial ruleset by default.
If you’re trying to treat semantic models like durable products, BPA is one of the simplest ways to turn “tribal knowledge” into repeatable feedback.
bpa_results = fabric.run_model_bpa( dataset="Sales Semantic Model", workspace="Sales Analytics")bpa_results.head()
Putting it together: a simple “semantic model → data product” workflow
If you want a concrete way to start, here’s a practical four-step flow I recommend because it’s both lightweight and repeatable:
- Discover the surface area with
list_tables()andlist_measures()so you’re not guessing. - Extract at the right grain with
read_table()(or go straight to a curated extract withevaluate_dax()). - Compute canonical features with
evaluate_measure()—let the model be the source of business logic. - Operationalize quality and health with
get_model_calc_dependencies(),run_model_bpa(), andmodel_memory_analyzer().
Do that once, and you’ll feel the shift: your semantic model stops being the “thing at the end,” and starts behaving like the semantic contract in the middle of your stack—exactly where a data product should live.
Conclusion: make the model usable outside the report
Let’s recap what we covered.
- SemPy (Semantic Link) turns Fabric semantic models into something notebooks can use, not just something visuals can render.
- Four generic functions (
list_datasets,list_tables,list_measures,list_relationships) help you understand and document the model’s exposed surface. - Three consumption functions (
read_table,evaluate_measure,evaluate_dax) let you treat that surface as a reusable interface for analysis and ML. - Three “expensive capability” functions (
get_model_calc_dependencies,model_memory_analyzer,run_model_bpa) make governance, performance work, and impact analysis part of your everyday workflow instead of a special event.
If you’re looking for a next action: pick one semantic model your org already trusts, open a Fabric notebook, and run the generic discovery functions first. Once you can name the surface area, consuming it becomes a design choice—not a reinvention project.
That’s how semantic models become data products.