A Python client library for Microsoft 365, the SharePoint REST API and Microsoft Graph.
Python 3.10+ · typed models · fluent queries · deferred execution · 700+ runnable examples
- Two clients, one library —
ClientContextspeaks the SharePoint REST API;GraphClientspeaks Microsoft Graph. - Fluent and deferred — chain
.select(),.filter(),.expand(),.top(),.order_by()and only hit the wire when you callexecute_query(). - Typed models — every entity is a real Python class (
List,File,Team,Message, …), not a dictionary. - Copy-paste examples — a runnable script for every service and scenario under
examples/.
New here? Pick a client in Choose your client, then jump straight to a runnable snippet in Examples by product.
- Choose your client
- Installation
- Authentication
- Quick start
- Examples by product
- Common patterns
- Async / await
- Dependencies
- Contributing
- Support
The library ships two clients. Pick the one that matches the API you need.
ClientContext |
GraphClient |
|
|---|---|---|
| Talks to | SharePoint REST API v1 | Microsoft Graph (graph.microsoft.com) |
| Entry point | ClientContext("https://contoso.sharepoint.com/sites/team") |
GraphClient(tenant="contoso.onmicrosoft.com") |
| Best for | SharePoint lists, items, files, folders, search, permissions, site and tenant administration, taxonomy, webhooks | Outlook, OneDrive, Teams, OneNote, Planner, To Do, Entra ID, Intune, Purview, Bookings, reports, security… |
| SharePoint | Full fidelity | Partial — prefer ClientContext for SharePoint-only work |
| Typed models | List, ListItem, File, Folder, Web, Site, User … |
DriveItem, Team, Message, Event, User, Group, PlannerTask … |
| Auth | certificate, interactive, device flow, cookies, NTLM (on-prem) | client secret, certificate, interactive, device flow, ROPC |
| Examples | examples/sharepoint/ |
examples/ by product |
| API reference | SharePoint REST | Microsoft Graph |
Rule of thumb
- SharePoint lists, files, site or tenant administration →
ClientContext - Teams, Outlook, OneDrive, Entra ID, Intune, Purview, reports →
GraphClient - OneDrive files work with either client;
GraphClientis the modern, cross-service path.
pip install office365-rest-python-client
With uv:
uv add office365-rest-python-client
Directly from source:
pip install git+https://github.com/vgrem/office365-rest-python-client.git
Modern flows authenticate through Microsoft Entra ID using MSAL. Both clients support delegated (user) and app-only (application) access.
Important
SharePoint ACS app-only access was retired in April 2026 and SAML-based authentication in May 2026. Use the certificate, interactive or device-code flows below instead — see examples/sharepoint/auth/modern/ and the migration script migrate_from_saml_to_cert.py.
| Flow | Access | Method |
|---|---|---|
| Certificate | App-only | with_client_certificate(tenant, client_id, thumbprint, cert_path=...) |
| Interactive | Delegated (MFA) | with_interactive(tenant, client_id) |
| Device code | Delegated (MFA) | with_device_flow(tenant, client_id) |
| Username / password | Delegated | with_username_and_password(tenant, client_id, username, password) |
| Custom token | Either | with_access_token(token_func) |
| Browser cookies | Delegated | with_cookies(cookie_source) |
| NTLM | On-premises | ClientContext(url, allow_ntlm=True).with_user_credentials(username, password) |
| ACS / SAML | On-premises (legacy) | legacy app-only · legacy SAML |
Note
SharePoint app-only access requires a certificate. A client secret is not accepted by SharePoint REST API v1 — use GraphClient for secret-based app-only. See why a client secret doesn't work for SharePoint.
from office365.sharepoint.client_context import ClientContext ctx = ClientContext("https://contoso.sharepoint.com/sites/team").with_client_certificate( "contoso.onmicrosoft.com", client_id="00000000-0000-0000-0000-000000000000", thumbprint="AA11BB22CC33DD44EE55FF66AA77BB88CC99DD00", cert_path="./private.pem", ) web = ctx.web.get().execute_query() print(web.title)
Ready-made scripts: certificate · certificate (private key) · custom scopes · interactive · device flow · username/password · access token · cookies · NTLM
| Flow | Access | Method |
|---|---|---|
| Client secret | App-only | with_client_secret(client_id, client_secret) |
| Certificate | App-only | with_certificate(client_id, thumbprint, private_key) |
| Interactive | Delegated (MFA) | with_token_interactive(client_id) |
| Device code | Delegated (MFA) | with_device_flow(client_id) |
| Username / password | Delegated | with_username_and_password(client_id, username, password) |
| Custom token | Either | GraphClient(token_callback, tenant=...) |
from office365.graph_client import GraphClient client = GraphClient(tenant="contoso.onmicrosoft.com").with_client_secret( client_id="00000000-0000-0000-0000-000000000000", client_secret="your-client-secret", ) users = client.users.get().execute_query() print(len(users), "users")
Ready-made scripts: client secret · certificate · interactive · device flow · username/password · custom token callback
SharePoint list — read and write items
from office365.sharepoint.client_context import ClientContext ctx = ClientContext("https://contoso.sharepoint.com/sites/team").with_client_certificate( "contoso.onmicrosoft.com", client_id, thumbprint, cert_path="./private.pem" ) tasks = ctx.web.lists.get_by_title("Tasks") item = tasks.add_item({"Title": "Write the release notes"}).execute_query() print("Created item:", item.id) for task in tasks.items.get_all().execute_query(): print(task.properties["Title"])
Microsoft Graph — send an email
from office365.graph_client import GraphClient client = GraphClient(tenant="contoso.onmicrosoft.com").with_client_secret(client_id, client_secret) client.me.send_mail( subject="Hello from Graph API", body="This email was sent using the Microsoft Graph API.", to_recipients=["[email protected]"], ).execute_query()
Every snippet below is lifted from a runnable script. Follow the More → link for the full example and its required permissions.
# Lists and items tasks = ctx.web.lists.get_by_title("Tasks") item = tasks.add_item({"Title": "Ship it", "Status": "Active"}).execute_query() tasks.items.get_all().execute_query() # or .filter("Status eq 'Active'").get_all() # Files and folders folder = ctx.web.get_folder_by_server_relative_url("/sites/team/Shared Documents") with open("report.pdf", "rb") as f: file = folder.files.upload(f).execute_query() with open("report.pdf", "wb") as f: ctx.web.get_file_by_server_relative_path("/sites/team/Shared Documents/report.pdf").download(f).execute_query() # Large file (chunked upload session) with open("video.mp4", "rb") as f: folder.files.create_upload_session(f, 10 * 1024 * 1024).execute_query()
| Area | Examples |
|---|---|
| Lists and items | lists · listitems · views |
| Files and folders | files · folders |
| Sites and webs | sites · webs · hubsites |
| Search | search |
| Permissions and sharing | permissions · sharing |
| Fields and content types | fields · contenttypes |
| Pages and navigation | pages · navigation |
| Taxonomy and profiles | taxonomy · userprofile |
| Tenant administration | tenant · groups · users |
| Webhooks, migration, advanced | webhooks · migration · advanced |
uploaded = client.me.drive.root.upload_file("report.xlsx").execute_query() with open("report.xlsx", "wb") as f: client.me.drive.root.get_by_path("report.xlsx").download(f).execute_query()
team = client.teams.create_and_wait("Contoso Project", "All project collaboration").execute_query() channel = team.channels.add("Project Chat").execute_query() channel.messages.add("Hello team!").execute_query()
from datetime import datetime, timedelta, timezone # Send mail client.me.send_mail( subject="Hello from Graph API", body="This email was sent using the Microsoft Graph API.", to_recipients=["[email protected]"], ).execute_query() # Create a calendar event when = datetime.now(timezone.utc) + timedelta(days=1) client.me.calendar.events.add( subject="Team Lunch", body="Let's grab lunch together.", start=when, end=when + timedelta(hours=1), attendees=["[email protected]"], ).execute_query()
from office365.directory.users.password_profile import PasswordProfile from office365.directory.users.profile import UserProfile profile = UserProfile( displayName="Alex Wilber", userPrincipalName="[email protected]", mailNickname="alex", accountEnabled=True, passwordProfile=PasswordProfile(password="P@ssw0rd!", forceChangePasswordNextSignIn=True), ) user = client.users.add(profile).execute_query() print(user.display_name)
group = client.groups.find_by_name("My Sample Team", required=True).execute_query() plans = group.planner.plans.get().execute_query() task = client.planner.tasks.add("Update client list", plans[0].id).execute_query() print(task.title)
for task_list in client.me.todo.lists.get().execute_query(): print(task_list.display_name, len(task_list.tasks.get().execute_query()), "tasks")
with open("Sample.html", "rb") as page_html: page = client.me.onenote.pages.add(presentation_file=page_html).execute_query() print(page.links.oneNoteWebUrl)
More services — Admin, Reports, Defender, Intune, Purview, Bookings, Communications, Security, Insights, Backup Storage
announcement = client.admin.service_announcement health = announcement.health_overviews.get().execute_query() issues = announcement.issues.get().execute_query() messages = announcement.messages.get().execute_query()
report = client.reports.get_email_activity_counts("D30").execute_query() print(report.value) # CSV payload
alerts = client.security.alerts_v2.top(20).get().execute_query() for alert in alerts: print(alert)
devices = client.device_management.managed_devices.get().execute_query() for device in devices: print(device.device_name, device.operating_system, device.compliance_state)
labels = client.security.data_security_and_governance.sensitivity_labels.get().execute_query() for label in labels: print(label.display_name, label.id)
businesses = client.solutions.booking_businesses.get().execute_query() for business in businesses: print(business.display_name)
presence = client.users["[email protected]"].presence.get().execute_query() print(presence.availability, presence.activity)
host = client.security.threat_intelligence.hosts["contoso.com"].get().execute_query() reputation = host.reputation.get().execute_query() print(reputation.properties.get("score"))
for item in client.me.insights.trending.get().execute_query(): print(item.resource_reference)
backup = client.solutions.backup_restore backup.get().execute_query() print(backup.service_status.status)
Deferred execution. Requests are queued and sent only when you call execute_query(). Chain the fluent methods first, then execute once.
items = ( ctx.web.lists.get_by_title("Tasks") .items.select(["Title", "Status"]) .filter("Status eq 'Active'") .order_by("Title") .top(100) .get() .execute_query() )
Addressing resources. Entities are addressed by the key you already know, so you rarely need an opaque id: user principal name, site path or URL, file path, list title. get_by_url/get_by_path on client.sites fetch when you call execute_query(); keyed/UPN forms and driveItem.get_by_path are lazy, so chain .get() first.
user = client.users["[email protected]"].get().execute_query() # a user, by UPN site = client.sites.get_by_url("https://contoso.sharepoint.com/sites/marketing").execute_query() file = client.me.drive.root.get_by_path("Reports/Q1.xlsx").get().execute_query() items = ctx.web.lists.get_by_title("Tasks").items.get().execute_query()
Reading large collections. Use get_all() to follow paging automatically, and page_size to stay under server limits.
items = ctx.web.lists.get_by_title("Orders").items.get_all(page_size=2000).execute_query() files = ( ctx.web.get_folder_by_server_relative_url("/sites/team/Shared Documents") .get_files(recursive=True, page_size=2000) .execute_query() )
Batching. Dispatch many queued operations in one round trip. Raise concurrency to run batches in parallel; throttled sub-requests are retried individually, honoring Retry-After.
for row in rows: ctx.web.lists.get_by_title("Contacts").add_item(row) ctx.execute_batch() # sequential ctx.execute_batch(concurrency=5) # up to 5 batches in flight # Graph for user in new_users: client.users.add(user) client.execute_batch(concurrency=5)
Large lists (> 5,000 items). SharePoint refuses filtered/sorted queries on non-indexed columns. The library can tell you what to index and pre-flight the query.
from office365.sharepoint.listitems.caml import Caml, CamlQuery query = CamlQuery.builder().where(Caml.text("Status").eq("Active")).order_by("ID").row_limit(2000, paged=True).build() lst.check_query(query) # raises with a clear message if an index is missing print(lst.index_candidates(query)) # columns worth indexing lst.ensure_indexed("Status").execute_query() # create the index (builds in the background)
Import and export DataFrames. Optional helpers turn list items into pandas / Polars frames and back.
lst = ctx.web.lists.ensure_list("Orders").execute_query() df = lst.to_dataframe().execute_query().value lst.from_dataframe(df, chunksize=100).execute_query()
Every call that reaches the network has an await twin. Builders stay synchronous — only the terminal call changes. No extra dependency is required: by default the blocking HTTP call is handed to a worker thread, so the event loop stays free.
import asyncio async def main() -> None: web = await ctx.web.get().execute_query_async() print(web.title) asyncio.run(main())
execute_query_async(), execute_batch_async(), get_all_async(), execute_query_parallel_async(), streaming downloads/exports and long-running-operation polling all have async twins, and collections support async for. See the async guide.
Install the core package on its own, or add the extras you need:
| Extra | Adds | Use it for |
|---|---|---|
azure |
azure-storage-blob, cryptography |
Azure Blob Storage and certificate helpers |
excel |
openpyxl |
Reading and writing Excel workbooks |
examples |
faker |
Running the sample scripts |
ntlm |
requests-ntlm |
On-premises SharePoint with NTLM |
pandas |
pandas |
to_dataframe() / from_dataframe() |
parquet |
pyarrow |
Parquet import and export |
duckdb |
duckdb |
Querying exported data locally |
sql |
sqlalchemy |
SQL-backed import and export |
notebooks |
jupyter, nbformat, jinja2 |
Running the notebook samples |
pip install "office365-rest-python-client[pandas,excel]"Issues and pull requests are welcome — please use the issue tracker for bugs and ideas.
git clone https://github.com/vgrem/office365-rest-python-client.git
cd office365-rest-python-client
uv sync --all-extras
pytest -qSee CONTRIBUTING.md for the full development setup. Run the gate before submitting: ruff check ., ruff format --check ., pyright and pytest -q.
- Source and examples — github.com/vgrem/office365-rest-python-client
- Documentation — vgrem.github.io/office365-rest-python-client
- Changelog — CHANGELOG.md
- Questions — GitHub Discussions or tag
office365-rest-python-clienton Stack Overflow
If this project saves you time, please consider sponsoring its development. ⭐
Released under the MIT License.