idfpy
Type-safe Pydantic models for all EnergyPlus IDF object types, plus IDF file read/write and simulation execution, optimized for LLM tool calling and IDE auto-completion.
Auto-generated from Energy+.schema.epJSON version 26.2.0.
Features
- 881 object types as Pydantic v2 models with full validation
- 287 reference types with cross-object validation
- Forward navigation —
surface.zoneresolves one reference field;surface.referenced()returns every referenced object - Reverse navigation —
zone.referencing("Lights")finds all objects that reference a given object - Reference validation —
idf.validate()batch-checks all cross-object references for existence and type compatibility - Extension plugin system —
surface.area,.normal,.centroidvia auto-discovered geometry mixins with full IDE support - Case-insensitive Literal field matching (EnergyPlus IDF is case-insensitive)
- Extensible field support (vertices, schedule data, etc.)
- IDF read/write with positional field ordering
- epJSON read/write with auto-detection by file extension
to_dict()/from_dict()for in-memory dict conversion (ideal for LLM tool calls)- EnergyPlus simulation execution with ExpandObjects support
- Accepts both
snake_caseand original EnergyPlus schema key names
Why idfpy over eppy?
| idfpy | eppy | |
|---|---|---|
| No EnergyPlus IDD required at runtime | ✅ | ❌ |
| Type-safe field validation | ✅ Pydantic v2 | ❌ |
| epJSON read/write | ✅ | ❌ |
| Cross-reference validation | ✅ 287 ref groups | ❌ |
| Forward/reverse navigation | ✅ 2921 properties | ❌ |
| Surface geometry (area/normal) | ✅ ext plugin | ❌ |
to_dict() / from_dict() for LLM |
✅ | ❌ |
| Dependencies | 4 (pydantic, jinja2, loguru, typer) | 12+ (lxml, pyparsing...) |
Installation
pip install idfpy
Quick Start
from pathlib import Path
from idfpy import IDF
from idfpy.models import Version, Building, Zone
# Create an IDF
idf = IDF()
idf.add(Version())
idf.add(Building(name='MyBuilding', north_axis=0.0))
idf.add(Zone(name='Zone1'))
# Save as IDF
idf.save(Path('output.idf'))
# Save as epJSON
idf.save(Path('output.epjson'), output_type='epjson')
# Load (auto-detects format by extension)
idf = IDF.load(Path('existing.idf')) # IDF format
idf = IDF.load(Path('existing.epjson')) # epJSON format
# Invalid objects raise, with the source line for IDF text;
# strict=False skips them with a warning instead
idf = IDF.load(Path('existing.idf'), strict=False)
# Run simulation
from idfpy.sim import simulate
result = simulate(
Path('output.idf'), weather=Path('weather.epw'), output_dir=Path('results/')
)
print(result.success) # True / False
In-memory dict conversion
from pathlib import Path
from idfpy import IDF
idf = IDF.load(Path('model.idf'))
# IDF → dict (epJSON structure)
data = idf.to_dict()
# {
# "Building": {"MyBuilding": {"north_axis": 0.0, "terrain": "Suburbs"}},
# "Zone": {"Zone1": {"direction_of_relative_north": 0.0}},
# ...
# }
# dict → IDF
idf = IDF.from_dict(data)
Logging
idfpy keeps INFO and higher-level logs available through Loguru. Detailed per-object and internal progress messages use Loguru's TRACE level, so they do not appear in a handler configured at DEBUG level. To inspect these messages, configure a TRACE sink explicitly:
from loguru import logger
logger.add('idfpy.log', level='TRACE')
Object navigation
Every reference field generates a @property for direct forward navigation. referenced() returns all objects referenced by an object, while referencing() provides reverse navigation. These navigation methods and all query methods (get / has / all_of_type / remove) accept either an EnergyPlus type string, a Python class name, or the model class itself — passing the class preserves precise typing in your IDE.
from pathlib import Path
from idfpy import IDF
from idfpy.models import BuildingSurfaceDetailed, Zone
idf = IDF.load(Path('model.idf'))
# Forward navigation — resolve reference to target object
surface = idf.get(BuildingSurfaceDetailed, 'Wall1') # → BuildingSurfaceDetailed | None
surface.zone_name # "Zone1" (raw string, always works)
surface.zone # Zone object (resolved via IDF)
surface.construction # Construction object
surface.referenced() # [Construction object, Zone object, ...]
surface.referenced(Zone) # [Zone object]
# Reverse navigation — find all objects referencing a given object
zone = idf.get(Zone, 'Zone1')
zone.referencing(BuildingSurfaceDetailed) # → [Wall1, Wall2, ...]
zone.referencing('Lights') # → [OfficeLights, ...]
# Chained navigation
zone.referencing(BuildingSurfaceDetailed)[0].construction
Strict type-name validation (default)
Query methods raise UnknownObjectTypeError when the type name cannot be resolved — this surfaces typos immediately instead of returning an empty result. Pass strict=False for the legacy silent behavior.
from idfpy import UnknownObjectTypeError
try:
idf.get('BuildingSurface:detailed', 'Wall1') # note the lowercase 'd'
except UnknownObjectTypeError as e:
print(e) # → Unknown object type: 'BuildingSurface:detailed'. ...
# Opt-in legacy silent behavior
idf.get('BuildingSurface:detailed', 'Wall1', strict=False) # → None
Reference validation
from idfpy import IDF, RefValidationError
idf = IDF.load(Path('model.idf'))
# Batch check all cross-object references
errors = idf.validate()
for e in errors:
print(e)
# [missing] Lights/OffLights.schedule_name: "BadSched" not found in any of [ScheduleNames]
# Or raise on first broken reference set
try:
idf.validate_or_raise()
except RefValidationError as exc:
print(f'{len(exc.errors)} broken reference(s)')
Real-world Example
from pathlib import Path
from idfpy import IDF
# Load a DOE reference building
idf = IDF.load(Path('LargeOffice.idf'))
# Modify all exterior walls' insulation
for con_name, con in idf.all_of_type('Construction').items():
layer = con.outside_layer_ref
if layer and hasattr(layer, 'conductivity'):
print(f'{con.name}: k={layer.conductivity} W/m·K')
# Validate all references
errors = idf.validate()
print(f'{len(errors)} broken references')
Geometry extensions
Surface models include geometry properties via the built-in ext.geometry plugin — area, normal vector, and centroid are computed from vertices using Newell's method, with full IDE autocompletion.
from idfpy import IDF
from pathlib import Path
idf = IDF.load(Path('model.idf'))
surface = idf.get('BuildingSurface:Detailed', 'Wall1')
surface.area # 30.0 (m²)
surface.normal # (0.0, -1.0, 0.0) — outward unit normal
surface.centroid # (5.0, 0.0, 1.5)
surface.vertices_as_tuples # [(0,0,3), (0,0,0), (10,0,0), (10,0,3)]
window = idf.get('FenestrationSurface:Detailed', 'Win1')
window.area # 16.0 (m²)
Supported surface types: BuildingSurface:Detailed, FenestrationSurface:Detailed, Floor:Detailed, RoofCeiling:Detailed, Wall:Detailed, Shading:Building:Detailed, Shading:Site:Detailed, Shading:Zone:Detailed.
Creating custom plugins
Extensions live in idfpy/ext/ as sub-packages. Each plugin exposes a MIXIN_MAP that the code generator auto-discovers:
# idfpy/ext/thermal/__init__.py
from .mixins import ThermalPropertyMixin
MIXIN_MAP: dict[str, type] = {
'BuildingSurfaceDetailed': ThermalPropertyMixin,
}
# idfpy/ext/thermal/mixins.py
class ThermalPropertyMixin:
@property
def u_value(self) -> float:
"""Compute U-value from construction layers."""
...
After adding a plugin, re-run idfpy codegen to regenerate models — the mixin is injected into the class hierarchy and IDE autocompletion works immediately.
Container mutation
from idfpy.models import Zone
idf.remove(Zone, 'Zone1') # unbinds + unregisters references
idf.remove('Zone', 'Zone1') # string form (EnergyPlus or Python class name)
License
MIT
Metadata
Release files for idfpy 26.1.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| idfpy-26.1.6.tar.gz | 654.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| idfpy-26.1.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.3 MB
Release files / idfpy-26.1.6.tar.gz
| Download URL | idfpy-26.1.6.tar.gz |
|---|---|
| Size | 654.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
806878813c35eb80834d4679d96e31e3898b0e8e416e70ba4033fc5afbaf1701
|
|
BLAKE2b-256 checksum How to use checksums |
dcfec2bbc110358c477b57e9587e072308128cd4bb1a1cad7bf648a9a267e626
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency logRelease files / idfpy-26.1.6-py3-none-any.whl
| Download URL | idfpy-26.1.6-py3-none-any.whl |
|---|---|
| Size | 653.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
352d71498e556d7f893ab4bf552bf2919c2d32e6006c0b02e9d1b5c60b3ced0b
|
|
BLAKE2b-256 checksum How to use checksums |
610c9b277d5502bcc18ea2e919372d9611de88e9e891838cbb28963d6a5630d1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency log