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:
- You save a near-empty project from your own copy of Project once (
templates/template.mpp). - The library keeps every stream it doesn't understand byte-for-byte.
- 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.
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:
Durations honoured by Project— done (verified in Project M365)Resources & assignments— done (verified in Project M365)Calendars— project calendar, extra base calendars, per-task calendars — doneNotes, custom fields, WBS, constraints, deadlines— doneProject properties— document metadata, status date, currency — doneRound-trip fidelity—Savefrom Project after opening produces the same schedule- NoodlePlanner integration — markdown →
.mppexport
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pymppwriter-0.3.0.tar.gz.
File metadata
- Download URL: pymppwriter-0.3.0.tar.gz
- Upload date:
- Size: 63.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f7b0b763682449a4904857e2656a264d37f2a4456e75abab5cb3f9c792621f5
|
|
| MD5 |
d032b2774b3ebfa179a603c1a1d8f084
|
|
| BLAKE2b-256 |
8e08b4af8f496d0992edc6aad4ca9b965ba687aedae0cda29f679e59a8cabcd0
|
Provenance
The following attestation bundles were made for pymppwriter-0.3.0.tar.gz:
Publisher:
publish.yml on kevinmcaleer/pymppwriter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pymppwriter-0.3.0.tar.gz -
Subject digest:
9f7b0b763682449a4904857e2656a264d37f2a4456e75abab5cb3f9c792621f5 - Sigstore transparency entry: 2711649935
- Sigstore integration time:
-
Permalink:
kevinmcaleer/pymppwriter@840f2eebcc001c9a1cddd5f91b36d859a70cab36 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/kevinmcaleer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@840f2eebcc001c9a1cddd5f91b36d859a70cab36 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pymppwriter-0.3.0-py3-none-any.whl.
File metadata
- Download URL: pymppwriter-0.3.0-py3-none-any.whl
- Upload date:
- Size: 51.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
251901284ff177f8eeeb86da25d49475328c7b69e2fe1396ca81baf801552b41
|
|
| MD5 |
5cca78216139818038387f9cacd2ab19
|
|
| BLAKE2b-256 |
e18d4fc93944fedaa450718b2941099cdf7055a29f5c951af63d57a7822cd02d
|
Provenance
The following attestation bundles were made for pymppwriter-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on kevinmcaleer/pymppwriter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pymppwriter-0.3.0-py3-none-any.whl -
Subject digest:
251901284ff177f8eeeb86da25d49475328c7b69e2fe1396ca81baf801552b41 - Sigstore transparency entry: 2711650831
- Sigstore integration time:
-
Permalink:
kevinmcaleer/pymppwriter@840f2eebcc001c9a1cddd5f91b36d859a70cab36 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/kevinmcaleer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@840f2eebcc001c9a1cddd5f91b36d859a70cab36 -
Trigger Event:
release
-
Statement type: