← Back to Knowledge Hub Module 9 → 9.4

9.4 Common Pitfalls: Units, None Values, Missing Psets


Introduction

The script at the end of 9.3 worked cleanly. Every wall it was tested against had the expected quantity set, every value inside it was a usable number, and nothing needed interpreting before printing. Real files aren't always that cooperative, and a script that only works when everything is present, populated, and unambiguous in its meaning isn't a script that survives contact with a real project. This lesson covers three real-world conditions a robust extraction script has to handle: a property set that simply isn't there, a value that's there but unset, and a number that's there and set but whose actual unit isn't something the raw value tells you on its own. The first two are entirely valid model states, not errors. The third is an interpretation problem worth taking seriously rather than assuming away.

A Missing Property Set Is a Valid Model State

get_psets(element, qtos_only=True) doesn't guarantee the quantity set a script is looking for actually exists. If an element was authored without one, or an exporter didn't populate it for that element type, the returned dictionary simply won't have that key. This is expected behavior, not a malfunction, and 9.3 already introduced the correct response to it:

quantities = ifcopenshell.util.element.get_psets(wall, qtos_only=True)
qto = quantities.get("Qto_WallBaseQuantities")

if qto:
    ...
else:
    print(wall.Name, "- no base quantities found")

Using .get() rather than direct key access (quantities["Qto_WallBaseQuantities"]) is the difference between a missing set producing a clean, checkable None and producing an unhandled KeyError that stops the script. This distinction matters more once a script runs unattended against a batch of files rather than one file being watched by a person.

A Present Key Doesn't Guarantee a Usable Value

A property set existing, and even the specific property or quantity name existing as a key inside it, still doesn't guarantee there's a usable value behind it. An unset IFC value can be represented by a $ token in the underlying STEP file, and IfcOpenShell exposes that as None in Python once the data reaches something like the dictionary get_psets() returns, not a missing key, not an empty string, but None sitting right where a real number was expected.

This is a different condition from a missing set entirely, and code that only checks whether a set exists will walk straight past it:

qto = quantities.get("Qto_WallBaseQuantities")

if qto:
    net_volume = qto.get("NetVolume")
    if net_volume is not None:
        print(wall.Name, "-", net_volume)
    else:
        print(wall.Name, "- NetVolume present but unset")

The set exists, the key exists, and the value is still None. Skipping this check and passing the result straight into a calculation, adding it to a running total, for instance, either raises a TypeError immediately or, worse, gets silently coerced into something misleading depending on what the surrounding code does with it. Checking explicitly for None before treating a value as numeric is the reliable way to distinguish an unset value from a real one.

This is worth naming as a pattern in its own right, since it's easy to write code that assumes a key existing is the same as having a number: key exists does not mean value exists, and value exists does not mean value is usable. Both checks are needed, and they check different things.

A Number Without Its Unit Isn't Finished Yet

Even a clean, non-None number carries a hidden assumption: what unit is it actually in. IFC doesn't fix this globally. A project establishes its own unit system through IfcUnitAssignment, which associates unit definitions with the quantities used across the model, and a length unit in that assignment can be declared as plain meters or as millimeters, among other options, depending entirely on how the authoring software or user set the project up. The same physical quantity can be represented by very different numeric magnitudes depending on the model's own unit definitions, a wall's net side area might read 4.2 in one file and 4200.0 in another for what is genuinely the same physical size, with nothing in the raw number itself signaling which is which.

ifcopenshell.util.unit.calculate_unit_scale(model) exists specifically to resolve this for a project's default length unit, returning a scale factor for converting that unit to and from SI meters. That's a real, useful tool, and worth knowing exists. But applying it correctly to a specific quantity, especially anything beyond a plain length, area and volume don't scale the same way a length does, and a specific quantity can occasionally carry its own explicit unit override that differs from the project default entirely, is enough of its own topic that it deserves being treated properly rather than folded into a first extraction script. The lesson worth locking in here is the principle, not a shortcut: a numeric IFC value is not self-describing, and before converting it to anything, the value's actual unit context has to be determined first, not assumed.

For this lesson, the honest and correct move is to stop at the value itself, not convert it:

net_volume = qto.get("NetVolume")

if net_volume is None:
    print(wall.Name, "- NetVolume present but unset")
    continue

print(wall.Name, "-", net_volume, "(unit not yet resolved)")

A Defensive Version of 9.3's Script

Putting the first two conditions together, this is 9.3's script rewritten to survive a real, imperfect file rather than only the clean one it was demonstrated on, deliberately leaving unit conversion out rather than applying it carelessly:

import ifcopenshell
import ifcopenshell.util.element

model = ifcopenshell.open("/path/to/model.ifc")

walls = model.by_type("IfcWall")

for wall in walls:
    quantities = ifcopenshell.util.element.get_psets(wall, qtos_only=True)
    qto = quantities.get("Qto_WallBaseQuantities")

    if not qto:
        print(wall.Name, "- no base quantities found")
        continue

    net_volume = qto.get("NetVolume")

    if net_volume is None:
        print(wall.Name, "- NetVolume present but unset")
        continue

    print(wall.Name, "-", net_volume, "(unit not yet resolved)")

Two checks, three distinct outcomes, none of them crash the script or silently produce a misleading number: no quantity set, a quantity set with the value unset, and a real, present value clearly labeled as not yet unit-resolved rather than quietly treated as meters.

What's Next

Every script built across this module so far runs top to bottom against one hardcoded file path, printing results as it goes. The final lesson in this module takes that shape and turns it into something structured enough to reuse: functions instead of one long script, a shape that can process more than one file without being rewritten each time.

Key Takeaways

  • A missing property or quantity set is a valid model state, not an error. Use .get() rather than direct key access so a missing set returns None cleanly instead of raising an exception.
  • A property or quantity can exist as a key while its actual value is None. An unset IFC value can be represented by $ in the underlying STEP file, and IfcOpenShell exposes unset values as None in Python. Check for this explicitly before treating a value as numeric.
  • Key exists does not mean value exists, and value exists does not mean the value is ready to use. These are separate checks worth keeping separate.
  • A numeric IFC value is not self-describing. The same physical quantity can be represented by very different numeric magnitudes depending on the project's own unit definitions, established through IfcUnitAssignment.
  • Converting a value correctly requires knowing its actual unit context first, which is more than a single scale factor in the general case, especially once quantities beyond a plain length are involved. When that context hasn't been resolved yet, label a value as such rather than assuming it's already in a known unit.
  • Handling missing sets, unset values, and unresolved units is what separates a script that works on one clean demo file from one that survives a real, messy project.

Discuss this lesson

Comments use a free GitHub account — takes under a minute to create, and keeps discussions spam-free and permanently archived.