Read and write IFC files with the standard library. Instances, property sets, quantities, units, the spatial chain — and edits written back out — no geometry kernel, no compiled dependency, no schema download.
from ifcspf import Model, Index
model = Model.open("building.ifc")
index = Index(model)
for door in model.of_type("IfcDoor"): # subtypes included
print(model.attr(door, "Name"), # attributes by name, not position
index.flat(door), # {"Pset_DoorCommon.FireExit": True, …}
index.spatial(door)["storey"]) # where it isExtracted from two tools that had each written it once already — a quantity takeoff and a building-code checker — because the interesting part of both was never the parser.
| layer | what you get |
|---|---|
spf |
ISO 10303-21 tokeniser and parser: strings and their \X2\ escapes, references, enumerations, typed values, complex instances, comments, gzip, the header |
schema |
attributes addressed by name (attr(door, "OverallWidth")), subtype expansion (IfcWall finds IfcWallStandardCase), IFC4 / IFC2X3 differences — all as data tables |
units |
the project unit assignment resolved to SI factors, including conversion-based units (feet, inches), plus output conversion to mm, ft2, cy, … |
psets |
property sets, quantity sets and the spatial chain, converted to SI, with type-object inheritance and per-property unit overrides |
write |
instances back to ISO 10303-21: semantic round trip, byte-idempotent output, \X2\ string encoding, IFC GlobalId compression |
Type-level property sets are inherited. A door's fire rating often lives on
its IfcDoorType, not the occurrence. Index collects type sets first and lets
instance sets override them — the inheritance IFC intends — and marks which is
which (Value.from_type).
Per-property units override the project unit. IfcPropertySingleValue and
every IfcQuantity* may carry their own Unit. A file with millimetre lengths
and one property in metres is not exotic; it is a Tuesday.
Every measure comes out in SI, and says what it was. Value carries the
converted number, the raw number, the measure type (IFCLENGTHMEASURE) and the
kind (length), so a consumer converts once, deliberately, at its own edge.
value = index.get(door, "Pset_PlancheckDoor", "ClearWidth")
value.value # 0.88 — metres, whatever the file used
value.raw # 880.0 — as written
value.measure # 'IFCLENGTHMEASURE'
value.kind # 'length'
value.from_type # Falsepip install ifc-spfPython 3.11+. No dependencies.
Model.open(path) / Model.from_text(text) # .ifc and .ifc.gz
model.of_type("IfcWall", subtypes=True) -> [Entity]
model.attr(entity, "Name") / model.ref_attr(entity, "RelatingStructure")
model.get(ref) / model.resolve_all(refs) / model.referencing(entity, of_type=None)
model.schema # 'IFC4' | 'IFC2X3' | …
model.scale # UnitScale(length=0.001, area=1.0, …)
model.type_counts()
index = Index(model, inherit_type=True)
index.values(entity) # {set name: {property name: Value}}
index.flat(entity) # {"Pset.Name": value}
index.get(entity, "Pset_DoorCommon", "FireExit") # one Value, case-insensitive
index.find(entity, "FireExit") # by name, any set
index.spatial(entity) # {"space": …, "storey": …, "building": …, "site": …}
index.storey_elevation(entity) # metres
from ifcspf import loads, load, unwrap, convert, expand_types
convert(0.88, "mm") # 880.0 — SI base out to a declared unit
# editing — by name, refusing to guess
model.set_attr(wall, "Name", "Tường trục A") # unknown names raise KeyError
new = model.add("IfcWall", GlobalId=new_guid(), Name="W-09")
model.remove(entity) # raises DanglingReferenceError if referenced
model.remove(entity, force=True) # …strips the references too
model.save("edited.ifc") # or model.dumps() for the textThe written file is semantically identical to what was parsed (same instances,
same values) and writing is byte-idempotent, but layout is not preserved:
comments and whitespace go, numbers are respelt canonically (1.0E3 → 1000.0),
non-ASCII strings come out as \X2\ runs. Diff an edited file against a
previous write, not against the original export.
- No geometry. No swept solids, no BRep, no placement maths, no clash detection. Property-level tooling — takeoffs, code checks, audits, exports — does not need it, and pretending otherwise is how a reader becomes a kernel.
- No schema validation. It reads what the file states about itself, and it
writes what the instances say — an edit that violates the EXPRESS schema will
be written faithfully, not corrected. The one guard is referential:
remove()will not silently orphan references. - No layout preservation on write. Output is canonical, not a patch of the original text (see above).
If you need geometry or validation, use ifcopenshell — it is excellent, and this package is not trying to replace it. This is for the large class of jobs where pulling in a compiled IFC toolkit is the heaviest thing in the project.
- qto — quantity takeoff with pluggable cost classification
- plancheck — building code as machine-readable rules
MIT licensed.