Choosing ezdxf, pydwg, or ODA for Production CAD Pipelines
Choosing how to read CAD files reliably is the first decision in any pipeline that ingests Autodesk formats, and the answer depends far more on whether your inputs are DXF or DWG than on any single tool’s feature list. This guide compares three approaches within the Interoperability Decision Guides framework: pure-Python ezdxf, community DWG readers such as LibreDWG and the pydwg bindings, and the licensed ODA File Converter and Teigha/ODA SDK.
The decision matters because the three tools are not substitutes. ezdxf reads DXF perfectly and cannot open DWG at all; community readers open some DWG but carry coverage and stability risk; the ODA converter opens DWG reliably across the full version range but adds a licence and a binary dependency. Picking the wrong one does not fail at import — it fails weeks later when a production DWG lands on a headless runner that has no way to read it. As with every decision in the Interoperability Decision Guides section, the goal is to score the real options against coverage, fidelity, licensing, and operational fit before the choice is baked in.
Prerequisites
Before working through the decision, confirm your environment and assumptions:
- Python 3.9+ — the snippets use
pathlib, type hints, and f-strings.# python>=3.9 - ezdxf ≥ 1.1.0 — install with
pip install "ezdxf>=1.1.0". This is the reader for all DXF, whether the DXF was authored directly or produced by conversion from DWG. - ODA File Converter — a free-to-register desktop binary from the Open Design Alliance, needed only for the DWG route. On a server it requires a virtual display (
xvfb). Confirm it is onPATHasODAFileConverter. - xvfb (headless only) —
apt-get install -y xvfbprovidesxvfb-runfor driving the GUI converter without a display. - Knowledge of the DXF entity model — you should already be comfortable traversing model space and querying entities, as covered in the ezdxf Deep Dive. This guide is about reaching readable geometry, not about extracting it once you have it.
- Awareness of DWG’s constraints — the reasons DWG cannot be read in pure Python are documented in pydwg Integration and the DWG Proprietary Limitations reference.
Architectural Overview
The three approaches differ along a small number of measurable dimensions. The table below scores each on the variables that actually decide production suitability.
| Dimension | ezdxf | LibreDWG / pydwg | ODA File Converter / Teigha SDK |
|---|---|---|---|
| Reads DXF | Yes (R12–R2018) | Via conversion only | Writes DXF for ezdxf to read |
| Reads DWG | No | Partial, version-dependent | Yes, reliably |
| DWG version range | — | Subset, varies by release | R12 through AutoCAD 2018 (AC1032) and later |
| Proxy object handling | Exposes if present in DXF | Often dropped | Preserved through conversion (as proxies) |
| ACIS solid fidelity | Raw payload exposed | Frequently incomplete | Preserved as ACIS in the DXF |
| Licensing | Open source (MIT) | Open source (GPL, LibreDWG) | Free-to-register binary; SDK is commercial |
| Headless / CI | Native, trivial | Native (CLI/bindings) | Needs xvfb; GUI-derived binary |
| Throughput | High (pure parse) | Moderate, variable | Conversion adds a subprocess per file |
| Failure mode | Clear exception on non-DXF | Silent partial reads | Non-zero exit / empty output |
Two conclusions fall out of the table. First, ezdxf is not in competition with the DWG tools; it is the DXF reader that sits after whichever DWG strategy you pick. Second, the real contest is between community readers and the ODA converter, and it turns almost entirely on how much you value guaranteed version coverage and clear failure behaviour versus avoiding a licensed binary.
DWG version codes, for reference when validating what a converter must handle:
$ACADVER code |
AutoCAD release | Notes |
|---|---|---|
| AC1009 | R12 | Oldest DWG the ODA converter targets as DXF output |
| AC1015 | 2000–2002 | Widely present in legacy archives |
| AC1018 | 2004–2006 | Common in municipal datasets |
| AC1021 | 2007–2009 | Unicode layer names appear here |
| AC1024 | 2010–2012 | — |
| AC1027 | 2013–2017 | — |
| AC1032 | 2018+ | Current production DWG version |
Step-by-Step Implementation
1. Classify the source format
Never assume the extension is honest, but do start with it. A cheap suffix check routes the file; a magic-bytes check confirms it. DXF ASCII files begin with a 0\nSECTION group pair, and DWG files begin with an AC10xx version tag in their first six bytes.
# python>=3.9
from pathlib import Path
def classify_cad(path: Path) -> str:
"""Return 'dxf', 'dwg', or raise for anything unrecognised."""
head = path.read_bytes()[:6]
if head[:2] == b"AC" and head[2:6].isdigit():
return "dwg" # e.g. b"AC1032"
suffix = path.suffix.lower()
if suffix == ".dxf":
return "dxf"
if suffix == ".dwg":
return "dwg"
raise ValueError(f"Not a recognised CAD source: {path.name}")
2. Read DXF directly with ezdxf
If the source is DXF, there is no decision left — ezdxf is the answer, and no conversion or licence is involved. Validate the version header before traversing so unsupported revisions fail early.
# ezdxf>=1.1.0 | python>=3.9
import ezdxf
SUPPORTED = {"AC1009", "AC1015", "AC1018", "AC1021", "AC1024", "AC1027", "AC1032"}
def read_dxf(path: str) -> ezdxf.document.Drawing:
doc = ezdxf.readfile(path)
ver = doc.header.get("$ACADVER", "UNKNOWN")
if ver not in SUPPORTED:
raise ValueError(f"Unsupported DXF revision: {ver}")
return doc
3. Convert DWG with the ODA File Converter (production route)
If the source is DWG and the pipeline must run unattended across arbitrary versions, drive the ODA File Converter to produce DXF, then read that DXF with the function above. The converter is a GUI binary, so on a server it must be wrapped in xvfb-run. Its positional arguments are input directory, output directory, output version, output format, recurse flag, audit flag, and an optional filename filter.
# python>=3.9 (external: ODAFileConverter, xvfb-run)
import shutil
import subprocess
from pathlib import Path
def convert_dwg_to_dxf(dwg_path: Path, out_dir: Path, version: str = "ACAD2018") -> Path:
"""Convert a single DWG to DXF headlessly via the ODA File Converter."""
if shutil.which("ODAFileConverter") is None:
raise RuntimeError("ODAFileConverter not on PATH; DWG cannot be read.")
out_dir.mkdir(parents=True, exist_ok=True)
# args: in_dir out_dir out_version out_format recurse(0/1) audit(0/1) filter
result = subprocess.run(
[
"xvfb-run", "-a", "ODAFileConverter",
str(dwg_path.parent), str(out_dir),
version, "DXF", "0", "1", dwg_path.name,
],
capture_output=True,
text=True,
timeout=300,
)
dxf_path = out_dir / (dwg_path.stem + ".dxf")
if result.returncode != 0 or not dxf_path.exists():
raise RuntimeError(
f"ODA conversion failed for {dwg_path.name}: "
f"rc={result.returncode} stderr={result.stderr.strip()!r}"
)
return dxf_path
The audit flag (1) tells the converter to repair recoverable errors during conversion; leave it on for untrusted inputs. Because a hung GUI process would otherwise block a worker forever, the timeout is mandatory in production.
4. Compose the route
With classification, direct reading, and conversion in place, a single entry point implements the whole decision tree. This is the function the rest of a pipeline calls; it never has to know which strategy was used.
# ezdxf>=1.1.0 | python>=3.9
from pathlib import Path
import ezdxf
def open_cad(path: Path, work_dir: Path) -> ezdxf.document.Drawing:
kind = classify_cad(path)
if kind == "dxf":
return read_dxf(str(path))
# kind == "dwg": convert then read
dxf_path = convert_dwg_to_dxf(path, work_dir / "_dxf")
return read_dxf(str(dxf_path))
5. Assess fidelity before trusting the output
Reaching a readable document is necessary but not sufficient. Confirm the route preserved what the consumer needs. Proxy entities from vertical products and ACIS solids are the two things most often lost, so probe for them and log rather than assume.
# ezdxf>=1.1.0 | python>=3.9
import logging
def audit_fidelity(doc: ezdxf.document.Drawing) -> dict:
msp = doc.modelspace()
proxies = sum(1 for e in msp if e.dxftype() == "ACAD_PROXY_ENTITY")
solids = sum(1 for e in msp if e.dxftype() == "3DSOLID")
if proxies:
logging.warning("%d proxy entities present; geometry may be opaque", proxies)
return {"proxy_entities": proxies, "solids_3d": solids}
Edge Cases & Gotchas
DWG passed to ezdxf.readfile()
ezdxf.readfile() on a DWG raises, but the message is not always obvious to a caller who expected it to “just read CAD.” Classify first (Step 1) so the failure is a deliberate route decision, not an unhandled exception deep in a worker.
try:
doc = ezdxf.readfile(str(path))
except (ezdxf.DXFStructureError, IOError):
logging.error("Not readable as DXF; route through ODA if DWG: %s", path.name)
raise
ODA converter silently produces no output
The converter can exit 0 yet write nothing when the target version is incompatible or the input is corrupt. Always assert the expected output file exists, as Step 3 does; never trust the return code alone.
Proxy objects survive conversion but stay opaque
The ODA converter preserves proxy entities as proxies — it does not decode the custom objects that Civil 3D or Plant 3D wrote. If the consumer needs that geometry, no reader here will supply it; the fix is upstream, by exploding proxies in the authoring application. This is the same constraint documented under DWG Proprietary Limitations.
Community readers report partial success
LibreDWG and pydwg may open a file and return a subset of entities without signalling that the rest were skipped. This silent partiality is the core production risk. If you use them, compare an entity count against a known-good ODA conversion of the same file before trusting the result, and confine them to input sets whose versions you have verified.
Encoding of layer names in older DWG
DWG written before the 2007 (AC1021) Unicode transition may carry code-page-encoded layer names that surface as mojibake after conversion. Normalise names on read and keep the original bytes in your audit log so a mis-decode is recoverable.
xvfb collisions under parallelism
Running many xvfb-run invocations concurrently can collide on display numbers. The -a flag selects a free display automatically; keep it, and give each worker its own output directory to avoid two conversions racing on the same DXF path.
Validation & Testing
Encode the decision as a test so it cannot silently regress when a library or the converter is upgraded. The test below asserts that both routes reach a readable document and that a DWG conversion yields at least as many entities as a stored baseline.
# ezdxf>=1.1.0 | python>=3.9
from pathlib import Path
import ezdxf
def test_dxf_route_reads_baseline():
doc = ezdxf.readfile("tests/fixtures/plan_R2018.dxf")
assert doc.header.get("$ACADVER") == "AC1032"
assert sum(1 for _ in doc.modelspace()) >= 500
def test_dwg_route_matches_reference(tmp_path: Path):
# convert_dwg_to_dxf / classify_cad imported from the pipeline module
dxf_path = convert_dwg_to_dxf(Path("tests/fixtures/plan_R2018.dwg"), tmp_path)
doc = ezdxf.readfile(str(dxf_path))
count = sum(1 for _ in doc.modelspace())
# Baseline captured from a known-good conversion; guards against silent loss.
assert count >= 500, f"Conversion dropped entities: {count} < 500"
Keep one fixture per DWG version you support (R12 through AC1032) so a coverage regression in the converter or a reader shows up as a failing test rather than as missing geometry in production.
Performance & Scale
The DXF route is a pure parse and scales the way ezdxf does — bounded by entity count and I/O, comfortably thousands of entities per millisecond of iteration for typical drawings. The DWG route pays a fixed subprocess cost per file for conversion, which dominates for small files and amortises for large ones. Two levers matter at scale:
- Batch conversions per converter invocation. The ODA converter accepts a directory and a recurse flag; converting a directory in one call amortises process startup across many files far better than one subprocess per file. Trade this against the coarser error granularity of a batch.
- Separate the CPU-bound parse from the subprocess-bound conversion. Run conversions in a bounded pool sized to your CPU count and keep parsing in a separate stage, so a slow conversion does not starve parsing workers.
To choose a batch size, thread count, and regression threshold on evidence rather than guesswork, measure the parse stage directly. The benchmarking DXF parsing throughput in Python walkthrough gives a reusable harness for entities-per-second, megabytes-per-second, and peak memory, and shows how to turn those numbers into a CI regression gate.
FAQ
Can ezdxf read DWG files directly?
No. ezdxf reads and writes DXF only; it has no DWG parser. DWG is Autodesk’s closed binary format, and reading it requires either a community reverse-engineered library such as LibreDWG or a licensed converter such as the ODA File Converter, which produces DXF that ezdxf can then read. The pydwg Integration workflow covers the community-reader path in detail.
Is the ODA File Converter free to use in production?
The ODA File Converter is distributed free of charge under a registration and redistribution agreement, but it is not open source, and its licence terms govern redistribution. The underlying Teigha/ODA SDK, for embedding DWG support directly in your own application, is a paid commercial licence. Settle licensing at design time, not deployment, because it determines whether your workers can be licence-free containers.
Why not just use LibreDWG or pydwg for DWG in production?
Community DWG readers such as LibreDWG and the pydwg bindings cover only a subset of DWG versions and entity types, and coverage varies by release. They are viable for exploratory work or where inputs are constrained to known-good versions, but for unattended production across the full R12-to-2018 range the ODA converter is materially more reliable and, critically, fails loudly rather than returning partial reads.
How do I run the ODA File Converter on a headless server?
The ODA File Converter is a Qt GUI application, so it needs a display. On a headless server, wrap the invocation in xvfb-run -a to supply a virtual framebuffer, pass the input and output directories and the target DXF version as positional arguments, and check both the return code and that the expected output file exists. The converter writes DXF files that ezdxf then parses.
Related Pages
- Interoperability Decision Guides — the decision framework this guide belongs to, covering library, format, and storage choices end to end
- Benchmarking DXF Parsing Throughput in Python — measure entities-per-second, megabytes-per-second, and peak memory to size the parse stage and set CI gates
- DXF vs IFC for GIS Ingestion — the next decision once the source is readable: which interchange format carries the data GIS needs
- GeoPackage vs PostGIS for CAD Output — where the parsed geometry lands, scored on concurrency and query needs
- ezdxf Deep Dive — the entity model and traversal patterns for the DXF you reach through either route
- pydwg Integration — the community DWG reader path and its proxy-object and version constraints