Skip to main content

idfpy

PyPI Python 3.12+ License: MIT EnergyPlus 26.2 Autoupdate Docs Ask DeepWiki

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.zone resolves 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, .centroid via 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_case and 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

# 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 25.1.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for idfpy 25.1.4
File Size Uploaded
idfpy-25.1.4.tar.gz 632.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for idfpy 25.1.4
File Interpreter ABI Platform
idfpy-25.1.4-py3-none-any.whl Python 3 none any Details

Total release size: 1.3 MB

Release files / idfpy-25.1.4.tar.gz

Download URL idfpy-25.1.4.tar.gz
Size 632.5 kB
Tags Source
SHA-256 checksum
How to use checksums
662b6fb832e6f8ca86a0d208a1def24a37c16da97b685b287a25a7e558ae3ace
BLAKE2b-256 checksum
How to use checksums
8c23bd52c00af3db7c67610d5dd25fb788c81641ade8910686a15f36ae54d72a
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 8, 2026.

Transparency log

Release files / idfpy-25.1.4-py3-none-any.whl

Download URL idfpy-25.1.4-py3-none-any.whl
Size 630.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
da31f39bdde493b68c15b263572668c0adfe3c20c4527c572ac6b473e715b0de
BLAKE2b-256 checksum
How to use checksums
241d1fa8cd63d02bf5bfce3e6a4f6545ff5021944023c78a2c9055f820ffbb37
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 8, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page