Skip to content
datahacks.dev
XML

Reading Microsoft Project and Visio XML Exports in Python

Read a Microsoft Project XML plan into tasks with durations, start dates, percent complete and predecessors, and learn how Visio files are zipped XML.

6 min read
Summarize in AI
Reading Microsoft Project and Visio XML Exports in Python

Project plans are data. A task list with start dates, durations, completion and dependencies is the sort of thing you want to put in a spreadsheet, a database or a report, and Microsoft Project can export one as XML. The same goes for diagrams in Visio. If you have the file and not the application, an XML parser is enough to read them.

The shape of the file

plan.xmlXML
<Project xmlns="http://schemas.microsoft.com/project">
  <Name>Launch</Name><StartDate>2026-11-02T08:00:00</StartDate>
  <Tasks>
    <Task><UID>1</UID><ID>1</ID><Name>Design</Name><OutlineLevel>1</OutlineLevel><Duration>PT40H0M0S</Duration><Start>2026-11-02T08:00:00</Start><PercentComplete>100</PercentComplete></Task>
    <Task><UID>2</UID><ID>2</ID><Name>Build</Name><OutlineLevel>1</OutlineLevel><Duration>PT80H0M0S</Duration><Start>2026-11-09T08:00:00</Start><PercentComplete>25</PercentComplete>
      <PredecessorLink><PredecessorUID>1</PredecessorUID><Type>1</Type></PredecessorLink></Task>
    <Task><UID>3</UID><ID>3</ID><Name>Ship</Name><OutlineLevel>1</OutlineLevel><Duration>PT8H0M0S</Duration><Milestone>1</Milestone></Task>
  </Tasks>
</Project>

The root is Project in the namespace http://schemas.microsoft.com/project, and the export is large but regular: project-level fields (Name, StartDate, calendar settings), then Calendars, Tasks, Resources and Assignments. Each Task has two identifiers, which are easy to confuse: UID is the permanent unique id, and ID is the row number as shown in the application, which changes when someone inserts or moves a row. Use the UID as the key for anything you store, and for links between tasks. OutlineLevel gives the nesting (a summary task at level 1, its children at level 2), and the tasks appear in outline order with no explicit parent element, so you rebuild the tree from the levels.

Reading the tasks

read_more.pyPython
import re
from lxml import etree

P = {"p": "http://schemas.microsoft.com/project"}

def hours(d):
    m = re.fullmatch(r"PT(\d+)H(\d+)M(\d+)S", d)
    return int(m[1]) + int(m[2]) / 60 + int(m[3]) / 3600

for t in etree.parse("plan.xml").getroot().iterfind("p:Tasks/p:Task", P):
    print(t.findtext("p:ID", namespaces=P), t.findtext("p:Name", namespaces=P),
          "%.0f h" % hours(t.findtext("p:Duration", namespaces=P)),
          "start", t.findtext("p:Start", namespaces=P),
          "pct", t.findtext("p:PercentComplete", namespaces=P),
          "milestone", t.findtext("p:Milestone", namespaces=P),
          "pred", t.xpath("p:PredecessorLink/p:PredecessorUID/text()", namespaces=P))
Text
1 Design 40 h start 2026-11-02T08:00:00 pct 100 milestone None pred []
2 Build 80 h start 2026-11-09T08:00:00 pct 25 milestone None pred ['1']
3 Ship 8 h start None pct None milestone 1 pred []

Four things to read from this. First, the namespace: without the p: prefix map, Tasks/Task finds nothing, as everywhere else. Second, the duration is an ISO 8601 duration string, PT40H0M0S (period, time, 40 hours, 0 minutes, 0 seconds). Project expresses it in hours even for long tasks, so a three-week task is PT120H0M0S and never P3W. The regular expression handles that form; for full generality parse the whole ISO syntax or use a library. Third, those hours are working hours under the project calendar. 40 hours is a work week if the calendar says eight-hour days, and the elapsed time between start and finish depends on weekends and holidays, which are in the Calendars part. Do not subtract two dates and call it the duration.

Fourth, the milestone. Ship has Milestone equal to 1 and a duration of 8 hours in this file, which is a placeholder. Real milestones usually have a duration of zero. Either way, a total of the durations must skip milestones, and findtext returns None for tasks without the element, so the check is == "1". Also expect summary tasks (those with children at a deeper outline level) whose duration is the span of their children; adding them to the children counts the work twice.

The dependency shows in Build: it has a PredecessorLink whose PredecessorUID is 1, the UID of Design. The Type is a code for the kind of link (finish-to-start is 1 in the schema, and the other three kinds follow), and there may be a LinkLag. To draw the network, build a dictionary from UID to task, then follow the links by UID.

From plan to a table

A task table has one row per task: UID, ID, outline level, name, hours, start, finish, percent complete, milestone flag, summary flag, predecessor UIDs (joined, or in a second table). The tree is rebuilt with a stack over OutlineLevel. Dates are ISO strings without a time zone; keep them as local datetimes and do not attach a zone you do not know. From the table, ordinary questions become queries: how many hours remain (duration times (100 minus percent) over 100, for non-summary tasks), which tasks are late (planned finish earlier than today and not complete), which have no predecessor.

Percent complete is the author's estimate, not a measure. A report that sums it should weight by duration: sum(hours * pct) / sum(hours). For this file that is (40 × 100 + 80 × 25) / 120 = 50 percent, with the milestone excluded, where a plain average of 100 and 25 would say 62.5.

Visio

A .vsdx file is a zip, like a docx, and the content is XML. The drawing lives in visio/pages/page1.xml (one part per page), in the namespace http://schemas.microsoft.com/office/visio/2012/main. Shapes are Shape elements, with Text children for their labels, Cell elements for geometry and properties, and connectors that reference the shapes they join. To list the labels of a diagram, open the zip, parse each page and take the text of every shape. To rebuild the graph (a flowchart, an org chart), read the Connect elements, which pair a connector's id with the shape ids at its two ends. The older .vdx format was a single XML file. I did not generate a Visio file here, so I will not show output; the approach is the one in reading Office Open XML from docx and xlsx files, where I tested the zip-and-parse method.

Practical advice

Keep the raw file with the table you made. A plan is revised weekly, and the question "what did the plan say on the 3rd" is answered by the file, not by the table. Ask the person who exports from Project which version and which options they used, because the export schema grows with the product and some fields exist only in newer versions. And prefer the application's own report if the question is about scheduling: critical path, slack and resource leveling come from the scheduling engine, not from the XML, which holds the results as of the last calculation (and the file may have been saved with automatic calculation off, so stale).

For the family of formats, see common XML formats you will meet in real work, and for the namespace mechanics, how XML namespaces work. The Project XML schema (MSPDI) and the Visio file format are documented by Microsoft on Microsoft Learn; I could not confirm a stable page address for either, so search there for "Project XML data interchange schema" and "Visio file format". The XML rules they build on are in the XML 1.0 specification and Namespaces in XML. Try a namespaced path in the XPath tester.

When the application should do the export you need

You need a schedule calculation. Critical path, slack and leveled resources are computed by Project. Export those as reports instead of recomputing them.

You need a format other than XML. Project can export to Excel and CSV with the fields you choose, which is simpler when you only need a table.

The file is a template with macros. A binary .mpp file is not XML. Export to XML from the application first.

Several people edit the plan. Reading a copy gives you a snapshot. If you need current data, use the shared source of truth the team uses.

Also inTooling
Taggedhow-toxml
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.