google-docs
Google Docs for Python. Read any document as Markdown. Write Markdown into any document: headings, lists, tables, links and code, all in one atomic request. Fill templates, update sections, export PDFs.
from google_docs import DocsClient, OAuthCredentials
docs = DocsClient(OAuthCredentials.load("token.json"))
doc = docs.create("Q3 report", """
# Q3 report
Revenue grew **18%**. Details are in [the sheet](https://example.com/q3).
| Region | Revenue | Growth |
|--------|---------|--------|
| North | $1.24M | +22% |
| South | $0.81M | +9% |
- Hiring is on track
- 4 of 5 roles filled
- Next review: **October 30**
""")
print(doc.url)
print(docs.get(doc).to_markdown()) # and back again
Why
The Google Docs API edits by character index. Every insert shifts everything after it. Tables have no "fill this cell" call, so you work out each cell's index by hand. Making a list nested means typing tabs that the API then deletes, which moves your text again. Indexes are counted in UTF-16, so one emoji throws off the rest of the request. Writing a single formatted paragraph takes a dozen requests that all have to agree with each other.
Google publishes no high-level Python client for Docs. google-api-python-client only hands you that raw request format. For Sheets, gspread fills this gap. This library aims to do the same for Docs.
Install
pip install google-docs
pip install "google-docs[pandas]" # optional: Table.to_pandas()
Python 3.9+. The only dependencies are httpx and markdown-it-py.
Sign in
- In the Google Cloud console, enable the Google Docs API and the Google Drive API.
- Create an OAuth client of type Desktop app and download its
client_secret_*.json. - Run:
google-docs login --client-secrets client_secret_XXX.json
The token is saved to ~/.config/google-docs/token.json and refreshed automatically. DocsClient also accepts a raw access token string or any google.auth credentials object, such as a service account.
Read
doc = docs.get("https://docs.google.com/document/d/1AbC.../edit") # an ID or a URL
doc.title, doc.url, doc.revision_id
doc.to_markdown() # GitHub-flavoured Markdown
doc.text # plain text
for h in doc.headings:
print(" " * (h.heading_level or 0), h.text)
table = doc.tables[0]
table.values # [["Region", "Revenue"], ["North", "$1.24M"], ...]
table.records() # [{"Region": "North", "Revenue": "$1.24M"}, ...]
table.to_pandas()
doc.find("TODO") # [(start, end), ...] index ranges
doc.tab("Appendix").to_markdown() # documents with tabs
Everything is typed: Paragraph, TextRun (.bold, .link, .is_code...), Table, Cell, InlineImage, and Chip for people, smart links, dates and footnotes. Each object keeps its raw JSON in .raw.
For LLM and RAG pipelines:
to_markdown()keeps headings, lists, tables, links and footnotes, so a model sees the document's structure and not just a flat run of text.
Write
Every write method takes Markdown or a Content object. Every write goes out as a single batchUpdate, so it either applies completely or not at all.
docs.append(doc, "## Update\n\nShipped **v2**.")
docs.prepend(doc, "> Draft, do not share")
docs.insert_after(doc, "Risks", "- Vendor delay\n- Hiring freeze")
docs.replace_section(doc, "Status", "All systems **green** ✅") # keeps the heading
docs.replace_text(doc, "2025", "2026") # returns the count
Build content in Python when it comes from data:
from google_docs import Content, bold, italic, link, code
report = (
Content()
.title("Weekly sales")
.paragraph("Total: ", bold("$2.7M"), " (", italic("+12% WoW"), ")")
.heading("By region", 2)
.table([["Region", "Revenue"], *rows]) # or [df.columns, *df.values]
.bullets(["North", (1, "nested item"), "South"]) # (level, text) to nest
.numbered(["first", "second"])
.checklist(["ship it", "write the post"])
.code("SELECT * FROM sales")
.image("https://example.com/chart.png", width=400)
.page_break()
)
docs.append(doc, report)
Templates
Put {{placeholders}} in any Google Doc, then fill them:
from google_docs import md
result = docs.fill(doc, {
"name": "Ayşe Yılmaz", # plain text: replaced everywhere, headers too
"date": "30 October 2026",
"terms": md("""
1. Fully remote
2. 30 days of paid leave
| Benefit | Value |
|---|---|
| Equity | 0.2% |
"""), # rich content: lists and tables go where {{terms}} was
})
result.counts # {"name": 3, "date": 1, "terms": 1}
Or copy a template and fill the copy in one call (this needs the Drive API):
offer = docs.from_template(TEMPLATE_ID, "Offer – Ayşe Yılmaz", values)
docs.export(offer, "pdf", "offer.pdf") # also docx, odt, rtf, txt, md, html, epub
Async
AsyncDocsClient has exactly the same methods:
from google_docs import AsyncDocsClient
async with AsyncDocsClient(creds) as docs:
doc = await docs.create("Notes", "# Meeting notes")
await docs.append(doc, summary_markdown)
Both clients run the same operation code. Each operation is written once, without I/O, and only the HTTP transport differs, so the sync and async clients can't drift apart.
Test your own code without Google
google_docs.testing.FakeDocs is an in-memory Docs server. It models documents the way Google does: UTF-16 indexes, paragraphs that end in newlines, and table, row and cell markers. It rejects the same invalid requests Google rejects, such as inserting in the middle of a table or deleting the document's last newline.
from google_docs.testing import FakeDocs
def test_weekly_report():
fake = FakeDocs()
docs = fake.client() # or fake.async_client()
doc = build_weekly_report(docs) # your code
assert docs.get(doc).tables[0].records()[0]["Region"] == "North"
Command line
google-docs new "Release notes" CHANGELOG.md # prints the URL
google-docs cat <doc> > doc.md # a document as Markdown
cat notes.md | google-docs append <doc> -
google-docs fill <template> --copy "Offer: Ayşe" --set name=Ayşe --md terms=terms.md
google-docs export <doc> report.pdf
google-docs tabs <doc>
Markdown support
| Markdown | In Google Docs |
|---|---|
# … ###### |
Heading 1–6 |
**bold** *italic* ~~strike~~ |
Bold, italic, strikethrough |
`code` and fenced code blocks |
Roboto Mono on a light grey background |
[text](url) |
Link |
- item, 1. item, nested lists |
Bulleted and numbered lists, up to 9 levels |
- [ ] task |
Checkbox list |
| Tables | Native tables; the header row is bold, cells can have formatting |
 on its own line |
Inline image (the URL must be public) |
> quote |
Indented paragraph with a left border |
--- |
A horizontal line, drawn as a paragraph border |
Two trailing spaces, or \ at line end |
Line break inside the paragraph |
Reading back gives the same Markdown, so a Markdown → Docs → Markdown round trip is lossless for everything in this table. The test suite checks this. Some things can't round-trip: image alt text (the API can't set it), the checked state of checkboxes, and #anchor links (Docs heading IDs are generated), which are written as plain text.
How it works
A planner turns your content into one list of requests:
- It inserts everything first, at a cursor that only moves forward, so every range it has already computed stays valid.
- It fills table cells last to first, using the index layout of a freshly inserted table.
- It resets inherited styles, because new text takes on the style of its neighbours (otherwise appending after a heading would give you another heading).
- It applies formatting next, then bullets from the bottom up, since bullets delete their nesting tabs and shift the text after them.
Every length is counted in UTF-16 units, as the API counts them.
Each write carries the revision ID it read. If someone edits the document in between, Google refuses the write and nothing is applied. The client then reads the document again and retries once. Rate limits and 5xx errors are retried with backoff.
Scopes
| Scope | Needed for |
|---|---|
documents |
reading and writing documents |
drive.file |
copy, export and trash on documents your app created or opened |
drive |
the same on any document (google-docs login --full-drive) |
Status
Version 0.1, alpha. 92 tests on Python 3.9–3.13 run against FakeDocs, whose index model was checked step by step against the live Docs API with scripts/compare_with_google.py. examples/live_check.py passes all 9 checks against the real API (October 2026): Markdown round trip, append, insert after a heading, replace a section, rich template fill, replace text, tables and PDF export. Run it with your own account to check again.
This is a community project. It is not affiliated with or endorsed by Google.
License
Apache-2.0
Metadata
Release files for google-docs 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| google_docs-0.1.0.tar.gz | 60.1 kB | With provenance | Details for google_docs-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| google_docs-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for google_docs-0.1.0-py3-none-any.whl |
Total release size: 115.3 kB
Release files / google_docs-0.1.0.tar.gz
| Download URL | google_docs-0.1.0.tar.gz |
|---|---|
| Size | 60.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
830d64187dac86a9a89a306367ab6f773f68debe04c55015a35837c2d4700227
|
|
BLAKE2b-256 checksum How to use checksums |
db2366890d4ce231f4c7f3bceee55b6471d9262b3474de6ec059e1b5db3bb270
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logRelease files / google_docs-0.1.0-py3-none-any.whl
| Download URL | google_docs-0.1.0-py3-none-any.whl |
|---|---|
| Size | 55.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0cf05b633c911fea824e96541438d0f8eddaa6a67cc5e97ba6fa665953c61fd8
|
|
BLAKE2b-256 checksum How to use checksums |
9e38eef49a4a290a38035e7395f4a793e201bce6156892dd06edf1b1c062b844
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logSecurity for google-docs 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for google_docs-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.