Parsing DWG Files with Python ezdxf

This page covers the specific technique of reading CAD floor plans with the ezdxf library and turning their raw entities into clean, unit-aligned geometry, and it sits within the SVG/DWG Parsing Workflows collection of the floor-plan parsing reference. It is the native-CAD branch that has to converge on the same polygon contract as the SVG room-boundary extractor before any routing work begins.

What “parsing with ezdxf” actually means

ezdxf is a pure-Python reader/writer for the DXF interchange format — the ASCII (or binary) text dialect that CAD tools emit alongside their proprietary .dwg. Parsing here means loading a drawing into an in-memory document tree, querying its modelspace for the entities that carry navigable meaning (walls, doors, room labels), and decomposing each entity into plain coordinate primitives. It is not the same as opening the file in AutoCAD: there is no rendering engine, no COM automation, and no implicit unit handling. You get the exact geometry the drafter stored, quirks included.

That distinction matters because ezdxf.readfile() accepts DXF, not the binary .dwg container. A true .dwg must first pass through the ODA File Converter (or libredwg) to become DXF. Everything below assumes that conversion has already happened and you hold a .dxf on disk. The output you are steering toward is the GeoJSON FeatureCollection envelope the rest of the pipeline consumes, so absolute, metric, right-side-up coordinates are the non-negotiable contract — the same one earned for floor-level (Z-axis) logic and for projection into an indoor coordinate reference system downstream.

The call sequence from a binary DWG to metric segments A sequence diagram across four participants. The batch worker calls the ODA converter with the DWG path and receives text DXF back, or a non-zero exit that must be handled. It then calls ezdxf readfile, falling back to recover.readfile for damaged files. ezdxf queries the modelspace for LINE, LWPOLYLINE and INSERT entities and receives an entity iterator. For each INSERT, ezdxf calls virtual_entities on itself to explode the block into world coordinates. Finally it returns scaled metric segments to the worker. Converter, reader, modelspace query, block explode Batch worker ODA converter ezdxf Modelspace dwg2dxf(path) text DXF (or non-zero exit) readfile() / recover.readfile() query('LINE LWPOLYLINE INSERT') entity iterator virtual_entities() on each INSERT scaled metric segments recover.readfile() is the retry path for drawings that fail a strict read.

Two of these steps fail routinely. The converter exits non-zero on password- protected and truncated drawings, and a strict read raises on files a CAD application still opens — which is what recover.readfile() exists for.

Minimal working example

The snippet below is the whole core technique in one self-contained block: open a DXF defensively, pull every line-like entity off the wall floor-levels, and emit a flat list of (start, end) segment tuples ready for Shapely. It handles the two failures you will actually hit first — a structurally broken file and a binary .dwg mistakenly passed to a text reader.

import logging
from typing import List, Tuple
import ezdxf
from ezdxf import DXFStructureError

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("dwg_parse")

Segment = Tuple[Tuple[float, float], Tuple[float, float]]

def extract_wall_segments(path: str) -> List[Segment]:
    """Load a DXF and return wall LINE/LWPOLYLINE segments as coordinate pairs."""
    try:
        doc = ezdxf.readfile(path)
    except DXFStructureError as exc:
        logger.error("Corrupt DXF structure in %s: %s", path, exc)
        raise
    except UnicodeDecodeError as exc:
        raise RuntimeError("Binary DWG detected — convert to DXF first") from exc

    msp = doc.modelspace()
    segments: List[Segment] = []
    for e in msp.query('LINE LWPOLYLINE[layer=="A-WALL"]'):
        if e.dxftype() == "LINE":
            segments.append(((e.dxf.start.x, e.dxf.start.y), (e.dxf.end.x, e.dxf.end.y)))
        else:  # LWPOLYLINE: get_points() yields (x, y, start_w, end_w, bulge)
            pts = [(p[0], p[1]) for p in e.get_points()]
            segments += list(zip(pts, pts[1:]))
    logger.info("Extracted %d wall segments from %s", len(segments), path)
    return segments

The msp.query() string is ezdxf’s entity DSL: a space-separated list of DXF types, each optionally followed by an attribute predicate in brackets. From here the segments flow into Shapely for unioning and into the wall-and-door logic that classifies passable edges.

Parameter and entity reference

The fields you touch most when extracting navigable geometry, with the types ezdxf exposes them as:

What each DXF entity type yields, and the trap that comes with it A reference grid of six DXF entity types. LINE gives two endpoints but no width, so wall thickness is lost. LWPOLYLINE gives a vertex list that may be closed, with bulge values encoding arcs. The older POLYLINE form has a 3D variant that must be flattened. INSERT is a placed block reference whose coordinates are block-local until exploded. ARC and CIRCLE give a centre, radius and angles that must be discretised into segments. ACAD_PROXY_ENTITY yields nothing usable and is silently empty, so occurrences must be counted and reported rather than ignored. Six entity types, six different ways to lose geometry Entity type Geometry you get Trap LINE two endpoints no width — wall thickness is lost LWPOLYLINE vertex list, may be closed bulge values encode arcs POLYLINE vertex list (older form) △ 3D variant needs flattening INSERT a placed block reference △ coordinates are block-local ARC / CIRCLE centre, radius, angles must be discretised to segments ACAD_PROXY_ENTITY nothing usable △ silently empty — count and report Proxy entities are drawn by AutoCAD from a cached image; a parser sees an empty shell.

The last row is why plans go missing. A drawing full of proxy entities parses cleanly and produces almost no geometry, so the failure looks like an empty floor rather than an error.

Field / argument Type Default Notes
ezdxf.readfile(filename) str / path Reads DXF text only. Binary .dwg raises UnicodeDecodeError; convert via ODA/libredwg first.
doc.modelspace() Modelspace The drawing’s primary entity space. Paperspace layouts are separate and usually annotation-only.
entity.dxf.layer str "0" The floor-level/category name. Naming is unstandardised (A-WALL, WALLS, S-WALL); match case-insensitively.
entity.dxftype() str LINE, LWPOLYLINE, POLYLINE, INSERT, TEXT, MTEXT, ARC, SPLINE.
LWPOLYLINE.get_points() list[tuple] Yields (x, y, start_width, end_width, bulge); a non-zero bulge means the segment is an arc.
INSERT.attribs iterable Block attribute tags (door IDs, room numbers); read attr.dxf.tag / attr.dxf.text.
doc.header["$INSUNITS"] int 0 Unit code — 4=mm, 6=m, 1=inch. 0 means unitless; you must assume a scale.
query('TYPE[attr=="x"]') str Entity DSL. Combine types with spaces; predicates support ==, !=, <, >, and ? regex.

A bulge value on an LWPOLYLINE vertex is the single most overlooked field: ignoring it silently turns curved wall runs and door swings into straight chords. Use ezdxf.math.bulge_to_arc() to expand each bulged segment into sampled points before building geometry.

Common errors and fixes

1. UnicodeDecodeError on readfile(). Symptom: the loader throws before any entity is read. Root cause is a real binary .dwg (or a binary-DXF) handed to the text reader. Detect the format up front and route binaries to a converter rather than crashing the batch:

from ezdxf.recover import readfile as recover_readfile

def load_resilient(path: str):
    """Fall back to ezdxf.recover for files with minor structural damage."""
    try:
        return ezdxf.readfile(path)
    except (UnicodeDecodeError, DXFStructureError) as exc:
        logger.warning("Standard read failed (%s); attempting recover mode", exc)
        doc, auditor = recover_readfile(path)   # repairs many malformed exports
        if auditor.has_errors:
            logger.error("Unrecoverable: %d errors", len(auditor.errors))
        return doc

2. Proxy objects yield no geometry. Symptom: AutoCAD Architecture/MEP exports show ACAD_PROXY_ENTITY types whose walls and ducts never appear in your segment list. ezdxf cannot decompose proxies into primitives. Detect them, log a topology-affecting gap, and degrade to a bounding reference so the floor level is not silently incomplete:

def flag_proxies(entities) -> list:
    """Log proxy entities ezdxf cannot decompose so they aren't lost silently."""
    proxies = [e for e in entities if e.dxftype() in ("ACAD_PROXY_ENTITY", "PROXY_ENTITY")]
    for p in proxies:
        logger.warning("Proxy %s on %s — route to ODA/libredwg for full geometry",
                       p.dxf.handle, p.dxf.layer)
    return proxies

3. Geometry is mirrored or 1000x off scale. Symptom: rooms render flipped, or door widths read as “900 metres”. Root cause is an unhandled $INSUNITS mismatch — millimetre drawings treated as metres — or a Y-axis assumption that does not hold. Read the unit code explicitly and scale to metres before emitting anything:

_TO_METRES = {1: 0.0254, 4: 0.001, 5: 0.01, 6: 1.0}  # in, mm, cm, m

def metre_scale(doc) -> float:
    code = doc.header.get("$INSUNITS", 0)
    if code == 0:
        logger.warning("Unitless drawing; assuming millimetres")
        return 0.001
    return _TO_METRES.get(code, 1.0)

How this feeds the pipeline

The segments and attributes produced here are the CAD-sourced substrate the rest of the floor-plan pipeline binds to. Once scaled to metres they are unioned and snapped into closed rings, classified into passable edges by the wall and door detection algorithms, enriched with the labels and block attributes covered in attribute mapping from blueprints, and then emitted inside the same GeoJSON FeatureCollection envelope defined by the JSON schema for indoor map APIs. When a portfolio of buildings has to be processed at once, this loader is the unit of work driven by the async batch processing pipelines, and the SVG branch converges on the identical polygon contract so CAD-sourced and web-sourced floors are indistinguishable to the routing graph.

From binary .dwg to a metric GeoJSON FeatureCollection with ezdxf A snake-shaped pipeline. Top row, left to right: a binary .dwg input chip flows into an ODA / libredwg convert step that emits text DXF; that flows into ezdxf.readfile with a recover() fallback; that flows into a modelspace query that selects LINE, LWPOLYLINE and INSERT entities on wall, door and label floor levels. A connector turns down the right edge into the bottom row, which flows right to left: a proxy-flag and unit-scale step that detects ACAD_PROXY_ENTITY objects and scales by the $INSUNITS code to metres; then a metric segment list of (start, end) coordinate tuples; then the output GeoJSON FeatureCollection in absolute, Y-up metres. Binary .dwg to metric GeoJSON: the ezdxf parsing path text DXF entity DSL binary .dwg proprietary container not readable directly convert ODA / libredwg .dwg → .dxf ezdxf.readfile recover() fallback doc.modelspace() modelspace query LINE · LWPOLYLINE walls · doors · labels flag + scale proxy → log gap $INSUNITS → metres metric segments (start, end) tuples bulge → sampled arc GeoJSON FeatureCollection absolute · Y-up · metric

Frequently Asked Questions

When should I use `recover.readfile()` instead of `readfile()`?

Use readfile() first and fall back to recover.readfile() only on failure. The recover module runs a repair pass that tolerates structural damage a strict read rejects — truncated sections, malformed handles, tables referencing entities that are not there — and it returns an auditor object listing what it fixed. That audit is the point: a drawing that needed recovery is a drawing whose geometry you should trust slightly less, so the audit count belongs in the ingest report rather than being discarded. Reaching for recover.readfile() unconditionally works, but it is slower on healthy files and, worse, it hides the signal that a particular building’s export process is producing damaged output.

Why does my parsed drawing contain almost no geometry?

The most common cause is ACAD_PROXY_ENTITY objects. When a drawing was authored with a vertical application — an architectural or MEP add-on — and is opened without it, AutoCAD substitutes proxy objects that carry a cached raster preview rather than real entities. The drawing looks complete on screen and parses without error, and the modelspace query returns almost nothing. Count ACAD_PROXY_ENTITY occurrences during ingest and fail loudly when they exceed a small threshold; the fix is to have the drawing re-exported from an application that can explode them, which is a conversation with the draughting team rather than a code change. The second most common cause is a floor-level filter that matches no layer, which produces the same empty result for a completely different reason and is worth distinguishing in the log.

Do I need the ODA converter, or can ezdxf read DWG directly?

ezdxf reads DXF, not binary DWG, so a DWG corpus needs a conversion step — usually the ODA File Converter or dwg2dxf — before ezdxf sees it. That hop is a real operational dependency: it is a separate binary in the container image, it exits non-zero on password-protected and truncated files, and it adds a couple of seconds per drawing. Whether to accept it or reach for a native DWG reader instead is the subject of ezdxf vs. LibreDWG for batch DWG parsing; for most teams the answer is that the conversion hop is worth ezdxf’s API and its permissive licence.

This page belongs to the SVG/DWG Parsing Workflows collection, part of the Automated Floor Plan Parsing & Vectorization reference.