← Back to Knowledge Hub Module 9 → 9.5

9.5 From Script to Reusable Tool


Introduction

Every script built across this module so far does the same thing: opens one hardcoded file, prints results to the screen, and stops. That's the right shape for learning, since it keeps every new idea isolated and easy to follow. It's the wrong shape for actually using the code, because running it against a different file means editing the script itself, and running it against ten files means editing it ten times. This lesson takes the defensive extraction logic from 9.3 and 9.4 and reshapes it into something that can be pointed at any file, and any number of files, without being rewritten each time.

Why a Script Isn't a Tool Yet

The scripts so far mix three separate jobs into one block of code: opening a file, extracting data from it, and printing that data. As long as those three things are welded together, the script can only ever do exactly what it currently does, to exactly one file. The fix isn't more code. It's separating those jobs so each one can be reused on its own terms.

Turning a Script Into a Function

The extraction logic from 9.4 already does real work. Wrapping it in a function changes what happens to the results, from printing them immediately to returning them as data:

import ifcopenshell
import ifcopenshell.util.element

def extract_wall_quantities(file_path):
    model = ifcopenshell.open(file_path)
    walls = model.by_type("IfcWall")

    results = []
    for wall in walls:
        quantities = ifcopenshell.util.element.get_psets(wall, qtos_only=True)
        qto = quantities.get("Qto_WallBaseQuantities")
        net_volume = qto.get("NetVolume") if qto else None

        results.append({
            "file": file_path,
            "wall_name": wall.Name,
            "global_id": wall.GlobalId,
            "net_volume": net_volume,
        })

    return results

Nothing about the extraction logic itself changed from 9.4. What changed is the ending: instead of printing each wall's result as it goes, the function builds a list of dictionaries, one per wall, and hands the whole list back to whatever called it. A missing quantity set or an unset value still doesn't crash anything, it just becomes None in that wall's dictionary, a real, checkable result rather than a printed line that's gone the moment the script finishes.

This is the actual shift this lesson is about. A script that prints is only useful the moment it's run, watched by a person. A function that returns data can be called from anywhere, checked, combined with other results, or handed to a second piece of code that does something else with it entirely.

Processing More Than One File

Once extraction returns data instead of printing it, running the same logic against several files stops being a rewrite and becomes a loop:

file_paths = [
    "/path/to/building_a.ifc",
    "/path/to/building_b.ifc",
    "/path/to/building_c.ifc",
]

all_results = []
for path in file_paths:
    all_results.extend(extract_wall_quantities(path))

print(f"Processed {len(file_paths)} files, {len(all_results)} walls total")

Nothing here is specific to IFC. It's the same pattern any batch task uses: a list of things to process, a function that handles one of them, and a loop that calls it repeatedly and collects the results. The only reason this was out of reach before is that the earlier version of the script had no clean boundary between "get the data" and "do something with the data." Now that boundary exists, and everything downstream gets simpler because of it.

Turning Results Into Something Usable

A list of dictionaries sitting in memory disappears the moment the script ends. Writing it out to a file is what makes the results actually usable afterward, and for a flat list of dictionaries with the same keys in each one, Python's built-in csv module handles this directly, no extra library needed:

import csv

def write_results_csv(results, output_path):
    if not results:
        return

    fieldnames = results[0].keys()
    with open(output_path, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(results)

write_results_csv(all_results, "wall_quantities.csv")

csv.DictWriter takes the same dictionary shape extract_wall_quantities() already produces and writes it straight to a spreadsheet-readable file, one row per wall, columns matching the dictionary keys exactly. This is a genuinely small amount of code for a real result: a script that started this module only able to print one file's data to a terminal now produces an actual file, covering as many source files as it was given, that opens directly in any spreadsheet tool.

Worth knowing this exact task, exporting IFC data to CSV, XLSX, or ODS with a query-based selection syntax, already exists as a dedicated, more capable tool within IfcOpenShell itself, called IfcCSV. Building a small version by hand here is about understanding the shape of the problem, not about replacing a tool that already solves it properly.

Where This Deliberately Stops

This is a taste of what a real extraction tool looks like, not a finished one, and the gap between the two is worth naming plainly rather than glossing over. A real tool needs to keep working when one file in a batch is corrupted or unreadable, rather than stopping the whole run. It needs some way of tracking confidence or completeness across a large batch, not just raw values. It needs proper packaging, so it can be run without opening a code editor first, and versioning, so results stay comparable as the code itself changes over time. None of that is covered here, and none of it belongs in an introductory lesson. What this lesson covers is the specific shift that makes all of that possible in the first place: separating extraction from output, and building around functions that return data rather than scripts that only print it.

Closing the Module

This module started with confirming a library installs correctly and ends with a small, genuinely reusable extraction tool built from nothing but that installed library and the standard tools already in Python. Every lesson in between, opening a file, reading direct attributes, reaching properties and quantities through the right relationship, and handling the real conditions a file can present, was a piece this final shape depends on. None of it stands alone; each lesson's script was quietly the foundation the next one built on.

The natural next step from here isn't more IFC-specific code. It's what happens around code like this at scale: running it automatically rather than by hand, checking its own output for problems, and fitting it into a repeatable pipeline rather than a script someone remembers to run. That's where Module 10 picks up.

Key Takeaways

  • A script that prints results directly is only useful at the moment it runs. Wrapping the same logic in a function that returns structured data, a list of dictionaries here, makes it reusable by anything that calls it.
  • Once extraction returns data instead of printing it, processing multiple files becomes a simple loop calling the same function repeatedly, not a rewrite of the script itself.
  • Python's built-in csv module, specifically csv.DictWriter, turns a list of same-shaped dictionaries into a real spreadsheet-readable file with very little code.
  • IfcOpenShell already provides a dedicated, more capable tool for exactly this kind of export, called IfcCSV. Building a small version here teaches the underlying shape of the problem rather than replacing a tool that already exists.
  • A real extraction tool needs far more than this: resilience to bad files, some notion of confidence or completeness, proper packaging, and versioning. This lesson deliberately stops short of all of that, since the actual shift worth learning here is functions over scripts, and returned data over printed output.

Discuss this lesson

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