Extracting IFC Property Sets with ifcopenshell
The direct answer: call ifcopenshell.util.element.get_psets(element) on any element and you receive a nested dictionary of the shape {pset_name: {property_name: value}}, with descriptive property sets and Qto_ quantity sets both resolved into plain Python types. This helper walks the IsDefinedBy relationship chain for you, unwraps typed IFC values, and — with its default settings — merges property sets inherited from the element’s type definition. It replaces dozens of lines of brittle manual traversal. For the wider set of parsing and geometry tasks this fits into, see the ifcopenshell Workflow guide.
How ifcopenshell Handles Property Sets
In the IFC schema, properties are not stored on an element as ordinary attributes. They hang off it through an objectified relationship. An IfcElement carries an inverse attribute IsDefinedBy, which points to one or more IfcRelDefinesByProperties relationships; each of those references an IfcPropertySet (for descriptive data) or an IfcElementQuantity (for measured quantities). The property set, in turn, holds a list of HasProperties, and each property is itself a typed entity.
Traversing that chain by hand is where most extraction code goes wrong. ifcopenshell.util.element.get_psets(element) does the traversal and returns a dictionary. A single external wall might resolve to:
{
"Pset_WallCommon": {"id": 1423, "FireRating": "REI60", "IsExternal": True},
"Qto_WallBaseQuantities": {"id": 1487, "Length": 5400.0, "NetVolume": 1.62},
}
Two things about that result surprise people. First, every inner dict contains an id key holding the STEP line number of the IfcPropertySet or IfcElementQuantity — useful for round-tripping, but noise when you flatten. Second, the values are already native Python types: the helper unwraps the IfcLabel, IfcBoolean, and IfcVolumeMeasure wrappers so you never touch .wrappedValue.
What the API does not do:
- It does not attach units.
NetVolumeabove is a bare1.62— a magnitude in the file’s native unit, not apint-style quantity. - It does not, by default, separate quantities from descriptive properties. Pass
psets_only=Trueorqtos_only=Trueto split them. - It does not fully flatten every exotic property type.
IfcPropertyTableValueand deeply nestedIfcComplexPropertystructures need theverbose=Truemode or a manual fallback (covered below).
The property types you will meet, in rough order of frequency:
IfcPropertySingleValue— oneNominalValue. The common case; flattens to a scalar.IfcPropertyEnumeratedValue— a chosen set of values from an enumeration; flattens to a list.IfcPropertyListValue— an ordered list of values; flattens to a list.IfcPropertyTableValue— pairedDefiningValues/DefinedValuesarrays (a lookup curve); best kept as parallel arrays.IfcComplexProperty— a named group of nested sub-properties; flattens to a nested dict.
The diagram traces the relationship path the helper resolves, including the type-level inheritance branch.
Production-Ready Script
The script below reads every IfcElement, merges descriptive and quantity sets into one wide record per element keyed by GlobalId, and writes newline-delimited JSON. If pandas and pyarrow are present it also writes a columnar Parquet file, which is the better target for a warehouse or PostGIS staging table.
# ifcopenshell>=0.8.0, Python 3.9+
import ifcopenshell
import ifcopenshell.util.element
import ifcopenshell.util.unit
import json
from pathlib import Path
from typing import Any
def _clean_set(props: dict) -> dict:
"""Drop the internal 'id' key that get_psets injects per set."""
return {k: v for k, v in props.items() if k != "id"}
def _flatten(psets: dict, qtos: dict) -> dict[str, Any]:
"""
Merge property sets and quantity sets into one flat dict.
Keys are 'SetName.PropertyName' so collisions across sets stay distinct.
List/array values (enumerations, table columns) are preserved as-is.
"""
flat: dict[str, Any] = {}
for set_name, props in {**psets, **qtos}.items():
for prop_name, value in _clean_set(props).items():
flat[f"{set_name}.{prop_name}"] = value
return flat
def extract_property_sets(ifc_path: str, element_query: str = "IfcElement") -> list[dict]:
"""
Extract all property sets and quantity sets for the queried elements.
Returns one record per element:
GlobalId (str), IfcType (str), Name (str | None), plus one key
per flattened 'PsetName.Property' entry.
"""
model = ifcopenshell.open(ifc_path)
# Length unit scale: multiply length-type quantities to reach meters.
# Areas scale by scale**2 and volumes by scale**3 (applied downstream).
length_scale = ifcopenshell.util.unit.calculate_unit_scale(model)
records: list[dict] = []
for element in model.by_type(element_query):
# psets_only / qtos_only split descriptive data from measured quantities.
# should_inherit=True (default) folds in the element type's property sets.
psets = ifcopenshell.util.element.get_psets(element, psets_only=True)
qtos = ifcopenshell.util.element.get_psets(element, qtos_only=True)
record: dict[str, Any] = {
"GlobalId": element.GlobalId,
"IfcType": element.is_a(),
"Name": getattr(element, "Name", None),
"_length_unit_scale": length_scale,
}
record.update(_flatten(psets, qtos))
records.append(record)
return records
def write_ndjson(records: list[dict], path: str) -> None:
with open(path, "w", encoding="utf-8") as fh:
for rec in records:
fh.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
def write_parquet(records: list[dict], path: str) -> bool:
"""Write a columnar Parquet table. Returns False if pandas is unavailable."""
try:
import pandas as pd # pandas>=2.0.0, pyarrow>=14.0.0
except ImportError:
return False
# A sparse, wide schema: every distinct 'Pset.Property' becomes a column.
# pandas fills absent keys with NaN, which is the correct sparse semantics.
pd.DataFrame(records).to_parquet(path, engine="pyarrow", index=False)
return True
if __name__ == "__main__":
recs = extract_property_sets("model.ifc", element_query="IfcElement")
print(f"Extracted property data for {len(recs)} elements")
write_ndjson(recs, "psets.ndjson")
if write_parquet(recs, "psets.parquet"):
print("Wrote psets.parquet (columnar)")
else:
print("pandas/pyarrow not installed — NDJSON only")
Key implementation notes:
- The
_clean_sethelper strips theidkey. Skip it and every element emits a meaningless integer column such asPset_WallCommon.id, which pollutes both the JSON and the Parquet schema. - Descriptive and quantity data are read in two calls (
psets_onlyandqtos_only) rather than one. That keeps aQto_length from silently sharing a column name with a same-named descriptive property, and it lets you apply unit scaling to quantities alone later. default=stronjson.dumpsis a safety net for the rare property value that resolves to a non-JSON-native type (for example a nested tuple from anIfcPropertyTableValue). It serializes deterministically instead of raising.- The wide, sparse schema is deliberate. Different element classes carry different property sets, so most cells are empty. Parquet stores that sparsity efficiently; a normalized long table (
GlobalId, key, value) is the alternative when you feed a relational store — see Mapping IFC Property Sets to PostGIS Columns for that shape.
To carry these attributes onto geometry rather than a flat table, pair this extraction with Batch Converting IFC to GeoJSON with ifcopenshell, which attaches the same flattened records as feature properties.
Compatibility Matrix
| Component | Supported Range | Notes |
|---|---|---|
| Python | 3.9 – 3.12 | Uses PEP 585 builtin generics (list[dict]); 3.9 needs from __future__ import annotations only for runtime-evaluated hints |
| ifcopenshell | ≥ 0.8.0 | get_psets / get_pset are stable since 0.7.0; the verbose, psets_only, qtos_only, and should_inherit keyword arguments require ≥ 0.7.0 |
| ifcopenshell (0.7.x) | Works | Same util.element API; the 0.8 packaging renamed some geometry settings but left util.element unchanged |
| pandas | ≥ 2.0.0 | Optional; only needed for the Parquet path |
| pyarrow | ≥ 14.0.0 | Parquet engine; fastparquet also works but handles sparse wide frames less predictably |
| IFC schema | IFC2x3, IFC4, IFC4x3 | Property set mechanism is schema-stable; IFC4 adds IfcElementQuantity sub-quantity types like IfcPhysicalComplexQuantity |
Fallback Strategies
1. IfcPropertyTableValue collapses to a single value or None
Table properties store two parallel arrays — DefiningValues and DefinedValues — that describe a lookup curve (for example, a thermal transmittance against temperature). The flat helper cannot represent both columns as one scalar. Read them directly and keep them as arrays:
# ifcopenshell>=0.8.0
def read_table_properties(element) -> dict[str, dict]:
tables: dict[str, dict] = {}
for rel in getattr(element, "IsDefinedBy", []):
if not rel.is_a("IfcRelDefinesByProperties"):
continue
pset = rel.RelatingPropertyDefinition
if not pset.is_a("IfcPropertySet"):
continue
for prop in pset.HasProperties or []:
if prop.is_a("IfcPropertyTableValue"):
tables[prop.Name] = {
"defining": [v.wrappedValue for v in prop.DefiningValues or []],
"defined": [v.wrappedValue for v in prop.DefinedValues or []],
}
return tables
2. Nested IfcComplexProperty structures flatten ambiguously
A complex property groups sub-properties under one name. Enable verbose mode so the helper returns nested dicts you can descend into, then decide whether to dot-join the nested keys:
# ifcopenshell>=0.8.0 — verbose returns richer structures for nested properties
psets = ifcopenshell.util.element.get_psets(element, verbose=True)
3. You need only instance properties, not inherited type properties
By default the helper folds in property sets defined on the element’s IfcTypeObject. When you are auditing what an author set on the instance itself, disable inheritance:
instance_only = ifcopenshell.util.element.get_psets(element, should_inherit=False)
4. Unit-bearing quantities look thousands of times too large
Qto_ lengths are raw magnitudes in the file’s native unit. A model authored in millimeters reports a Length of 5400.0 for a 5.4 m wall. Apply the scale factor, remembering the exponent for areas and volumes:
# ifcopenshell>=0.8.0
scale = ifcopenshell.util.unit.calculate_unit_scale(model) # 0.001 for mm
length_m = raw_length * scale
area_m2 = raw_area * scale ** 2
volume_m3 = raw_volume * scale ** 3
5. Fetching one known property is faster than reading every set
When you need a single value — say IsExternal — do not flatten the whole element. Use get_pset with the prop argument, which returns just that value or None:
# ifcopenshell>=0.8.0
is_external = ifcopenshell.util.element.get_pset(
element, "Pset_WallCommon", prop="IsExternal"
)
FAQ
Why does each property set dict contain an id key?
get_psets injects an id key holding the STEP line number of the IfcPropertySet or IfcElementQuantity itself. It is metadata, not a property. Drop keys named id before flattening, or you will emit a spurious integer column per set.
Does get_psets include type-level property sets?
Yes. With should_inherit left at its default of True, get_psets merges property sets attached to the element’s IfcTypeObject with those attached to the instance. Instance values override type values on key collisions. Pass should_inherit=False to read only instance-level sets.
How do I separate quantities from descriptive properties?
Call get_psets(element, psets_only=True) for IfcPropertySet data and get_psets(element, qtos_only=True) for IfcElementQuantity data. Quantity sets are conventionally named with a Qto_ prefix and carry lengths, areas, and volumes in the project’s base units.
What units do quantity values use?
get_psets returns the raw stored magnitude, not a unit. Read ifcopenshell.util.unit.calculate_unit_scale(model) to convert length quantities to meters, and remember area and volume scale by the square and cube of that factor respectively.
Related Pages
- ifcopenshell Workflow — parent guide covering the full ifcopenshell API surface for IFC parsing, geometry, and attributes
- Batch Converting IFC to GeoJSON with ifcopenshell — attach these flattened property records to element footprints as GeoJSON feature properties
- Extracting IFC Wall Geometries to Shapely — sibling workflow that produces the geometry these attributes describe
- Mapping IFC Property Sets to PostGIS Columns — turning the wide extract into a normalized relational schema for spatial queries