Skip to main content

pymppwriter

Write Microsoft Project .mpp files from pure Python. No Java, no .NET, no Microsoft Project installation, no commercial library. MIT licensed.

pymppwriter produces native MPP14 files (the format used by Project 2010 through the current Microsoft 365 desktop client). Files it writes open in Project by double-click — which is the whole point: an .mpp download is associated with Project on every corporate PC, whereas the MSPDI .xml export has to be opened manually from inside Project.

Status: alpha. Verified to open correctly in Project M365: task names, hierarchy, dates, durations (values, display units, estimated flags, milestones and summary rollups), dependencies, resources and assignments, calendars (working weeks, holidays, extra base calendars, per-task calendars), the wider task fields (notes, WBS, constraints, deadlines, progress, priority, type, custom text/number/date/flag fields, manual scheduling) and project properties (document metadata, status date, currency). Resource rates and costs, baselines and timephased data are not yet written. Treat this as a working proof-of-concept, not a product.

How it works

The MPP format is an undocumented OLE2 compound document. Reading it selectively is a solved problem (see MPXJ); writing one from scratch means reproducing ~170 KB of view definitions, Gantt bar styles, tables and filters that Project insists on.

pymppwriter sidesteps that with a template-and-patch approach:

  1. You save a near-empty project from your own copy of Project once (templates/template.mpp).
  2. The library keeps every stream it doesn't understand byte-for-byte.
  3. It rewrites only the task, dependency and project-property streams, cloning prototype records from the template and patching the fields it controls.

The field-offset map is read from the template's own Props stream, so the writer adapts to whatever Project version wrote the template. Details in docs/FORMAT_NOTES.md.

Installation

pip install git+https://github.com/kevinmcaleer/pymppwriter

Only runtime dependency: olefile (used to read the template; the container writer is our own).

Make your template (one-time)

In Microsoft Project: File → New → Blank Project, then

# Task Name Action
1 Task 1 leave as-is
2 Task 2 Indent it under Task 1 (Task 1 becomes a summary)
3 Task 3 select Tasks 2 and 3, Link Tasks (Finish-to-Start)

Don't add resources, baselines or calendar changes. Save As templates/template.mpp.

Why you must make it yourself: the template is a file written by Project, so it must come from a copy you're licensed to use, and it embeds your username. It is .gitignored.

Save the template from the same Project version that will open the generated files — several structures (calendar definitions in particular) are stored in version-specific dialects, and a template written by your own copy guarantees the output speaks the dialect your Project reads. Both current M365 and 2010-era templates are supported.

Usage

Command line

Describe the plan in JSON (examples/example_project.json):

{
  "title": "My plan",
  "start": "2026-09-07T08:00",
  "tasks": [
    {"uid": 1, "name": "Phase 1", "start": "2026-09-07T08:00", "finish": "2026-09-09T17:00", "duration_days": 3, "outline_level": 1},
    {"uid": 2, "name": "Do the thing", "start": "2026-09-07T08:00", "finish": "2026-09-08T17:00", "duration_days": 2, "outline_level": 2, "parent_uid": 1}
  ],
  "links": [ {"pred": 1, "succ": 2, "type": "FS", "lag_days": 0} ]
}
pymppwriter build examples/example_project.json --template templates/template.mpp --out plan.mpp
pymppwriter inspect plan.mpp        # dump the OLE stream tree

Python API

from datetime import datetime as D
from pymppwriter import MppWriter, Project, Task, Relation

project = Project(
    title="Robot build plan",
    start=D(2026, 10, 5, 8, 0),
    tasks=[
        Task(uid=1, name="Design",      start=D(2026,10,5,8),  finish=D(2026,10,9,17),  duration_days=5),
        Task(uid=2, name="Print parts", start=D(2026,10,12,8), finish=D(2026,10,14,17), duration_days=3),
        Task(uid=3, name="Assemble",    start=D(2026,10,15,8), finish=D(2026,10,16,17), duration_days=2,
             outline_level=1, parent_uid=0),
    ],
    relations=[Relation(1, 2), Relation(2, 3, type="FS", lag_days=0)],
)

MppWriter("templates/template.mpp").write(project, "robot-build.mpp")

Model reference

Class Field Notes
Project title, start, tasks, relations start sets the project start date
Task uid unique, > 0, stable across exports
name, start, finish datetimes
duration_days working days; 0 = milestone; ignored for summary tasks (rolled up from children in working time)
duration_units display units: "m", "h", "d" (default), "w", "mo"
estimated True shows the duration with a trailing ?
outline_level 1 = top level, 2 = child, …
parent_uid 0 = top level, else uid of the summary task
guid auto-generated; pass your own to keep GUIDs stable between exports
Relation pred_uid, succ_uid
type "FS" (default), "SS", "FF", "SF"
lag_days may be negative for lead
Resource uid unique, > 0
name, initials, email strings; only name is required
max_units 1.0 = 100% (default)
guid auto-generated; pass your own to keep GUIDs stable
Assignment task_uid, resource_uid must reference existing tasks/resources
units 1.0 = 100% (default); work is computed from the task's duration
Calendar name Project.calendar edits Standard; Project.calendars adds base calendars
week {weekday: ranges}; weekday 0=Mon..6=Sun; ranges = [(start_min, end_min), …] or None for non-working; missing days keep defaults
exceptions list of CalendarException(start, finish=None, name="") — non-working dates
CalendarException start, finish datetime.dates; finish defaults to start

Set Task.calendar to a calendar name to schedule that task on it, and Project.default_calendar to change the project calendar. Summary/rollup durations are computed in working time using Project.calendar's week and holidays.

"calendar": {"week": {"wed": [["08:00", "12:00"]], "sat": null},
             "holidays": ["2026-09-21", {"from": "2026-10-01", "to": "2026-10-02", "name": "Conf"}]},
"calendars": [ {"name": "Nights", "week": {"mon": [["18:00", "22:00"]]}} ]

In JSON specs, resources and assignments look like:

"resources": [ {"uid": 1, "name": "Kevin", "initials": "K", "max_units": 1.0} ],
"assignments": [ {"task": 1, "resource": 1, "units": 0.5} ]

Tasks are written in list order, which becomes the ID / row order in Project.

Baselines

from pymppwriter import MppWriter, Project, Task, set_baseline, clear_baseline

set_baseline(project)            # slot 0, the unnumbered Baseline
set_baseline(project, 3)         # Baseline3
clear_baseline(project, 0)
MppWriter("templates/template.mpp").write(project, "plan.mpp")

Saves the current schedule into one of the eleven slots, across all three entity classes:

on a baseline records
task start, finish, duration and work, with summaries spanning their children
assignment start, finish and work — the task's schedule scaled by the assignment's units
resource work and cost, added up from its assignments (Project stores no dates here)

read_project() returns them as task.baselines, resource.baselines and assignment.baselines, each a {slot: Baseline} dict. Baselines are stored as variable data, not the fixed record fields, which is what a Project-written reference showed; docs/FORMAT_NOTES.md has the layout, and the timephased baseline blobs that Project uses only for the usage views are not written.

Validation

MppWriter.write() validates the plan first and refuses to write a file Project would reject or silently repair: duplicate or non-positive task uids, parent references that contradict the outline levels, finishes before starts, links to unknown tasks, self-links and dependency cycles all raise ValueError. Two softer disagreements come back as ScheduleWarnings, because Project accepts the file but changes it: a task whose declared start is earlier than its predecessors allow (Project moves it on the next recalculation), and a task calendar that shares no working time with its assigned resources' calendars (Project opens with "Not enough common working time").

Declared starts that Project's scheduler would not produce on its own are held in place with a Start-No-Earlier-Than constraint, exactly as Project does for a typed-in date; tasks their predecessors already place are left as-soon-as-possible so plans stay link-driven.

Reading a plan back

from pymppwriter import read_project

project = read_project("plan.mpp")          # any MPP14 file, 2010 through M365
for task in project.tasks:
    print(task.uid, task.name, task.duration_days, task.percent_complete)

read_project() returns the same Project the writer takes, so a file can be read, edited and written again. Every offset comes from the file's own Props field maps and every flag from its meta bitmaps — there are no hard-coded record layouts — so it reads files saved by any Project version of that era, not just ones this library wrote. Tasks (names, dates, durations, outline, notes, WBS, constraints, progress, manual scheduling), dependencies with types and lags, resources and assignments come back; baselines, costs and timephased data do not. A file that is not an MPP14 project raises MppReadError.

Verifying output without Project

If you have Java installed, scripts/mpxj_oracle.py reads any .mpp back through MPXJ (pip install mpxj jpype1) and prints tasks, links and resources. scripts/analyze_mpp.py dumps the task records field-by-field — useful when diffing against a file Project saved.

Development

git clone https://github.com/kevinmcaleer/pymppwriter && cd pymppwriter
pip install -e ".[dev]"
pytest

The end-to-end test is skipped unless templates/template.mpp exists.

Roadmap

Tracked in the GitHub Project. Headline epics:

  1. Durations honoured by Project — done (verified in Project M365)
  2. Resources & assignments — done (verified in Project M365)
  3. Calendars — project calendar, extra base calendars, per-task calendars — done
  4. Notes, custom fields, WBS, constraints, deadlines — done
  5. Project properties — document metadata, status date, currency — done
  6. Round-trip fidelitySave from Project after opening produces the same schedule
  7. NoodlePlanner integration — markdown → .mpp export

Provenance & licensing

All format knowledge comes from the public [MS-CFB] specification, the observable read behaviour of the LGPL MPXJ library, and byte-diffing files saved by Microsoft Project. No code was derived from MPXJ or from any proprietary library, and no proprietary binaries were decompiled. The repository ships no .mpp files: MPXJ's test fixtures are LGPL and were used only as read-only references during development.

Microsoft Project is a trademark of Microsoft Corporation. This project is not affiliated with Microsoft.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pymppwriter-0.4.0.tar.gz (71.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pymppwriter-0.4.0-py3-none-any.whl (57.0 kB view details)

Uploaded Python 3

File details

Details for the file pymppwriter-0.4.0.tar.gz.

File metadata

  • Download URL: pymppwriter-0.4.0.tar.gz
  • Upload date:
  • Size: 71.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pymppwriter-0.4.0.tar.gz
Algorithm Hash digest
SHA256 a48dac615f0d410c3655e02ea8b2f4d61bfe49d77e06833e019239dba24dae89
MD5 0192e2e1166ec1368dd1242a7a85974b
BLAKE2b-256 80b07570b3b8e461b35142ff31232f3745d44fbf26d37589bf6a30af4106098c

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymppwriter-0.4.0.tar.gz:

Publisher: publish.yml on kevinmcaleer/pymppwriter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pymppwriter-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: pymppwriter-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 57.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pymppwriter-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d07c8f43bcffd825b27e1b3dd4992b66451b1ef02ed68f321d34bdc098a6e700
MD5 b21757181c80b6048e772e4232e1f45f
BLAKE2b-256 7f1ca0ca647b86ca0193d3c3268b584f2ce2308e3a88756cd9154367e8260478

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymppwriter-0.4.0-py3-none-any.whl:

Publisher: publish.yml on kevinmcaleer/pymppwriter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 files

0.3.0

2 files

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