Skip to main content

PowerPlatform Dataverse Client for Python

PyPI version Python License: MIT

The Dataverse SDK for Python lets Python developers access, manage, and manipulate Microsoft Dataverse business data using familiar Python syntax — no .NET knowledge required. It wraps the Dataverse Web API in a single typed client that works with tables and records as native Python dictionaries and pandas DataFrames.

Source code | Package (PyPI) | API reference | Product documentation | Samples

Table of contents

Key features

  • CRUD & bulk — single records plus native CreateMultiple / UpdateMultiple / UpsertMultiple / BulkDelete.
  • Fluent QueryBuilder — type-safe col() filters; plus read-only SQL and FetchXML.
  • Schema & relationships — create tables, columns, and 1:N / N:N relationships.
  • pandas DataFrames — read and write records as DataFrames and Series.
  • File uploads — to file columns, with automatic chunking for large files.
  • Batch — many operations per HTTP request, with transactional changesets.
  • Azure Identity auth & typed errors — any TokenCredential; structured exception hierarchy with retry guidance.
  • Async — AsyncDataverseClient mirrors the sync API.

Getting started

Prerequisites

  • Python 3.10 or later
  • A Microsoft Dataverse environment with appropriate permissions
  • OAuth authentication configured for your application

Install

pip install PowerPlatform-Dataverse-Client

The pandas library is installed automatically and powers the client.dataframe namespace. The async client requires an optional extra: pip install "PowerPlatform-Dataverse-Client[async]".

Authenticate

The client accepts any Azure Identity TokenCredential. Use InteractiveBrowserCredential for local development, or ClientSecretCredential / CertificateCredential for unattended production apps. For app registration and all credential types, see Use OAuth with Dataverse.

from azure.identity import InteractiveBrowserCredential

# Local development: opens a browser to sign in
credential = InteractiveBrowserCredential()

# Production (unattended) alternatives:
# from azure.identity import ClientSecretCredential, CertificateCredential
# credential = ClientSecretCredential(tenant_id, client_id, client_secret)
# credential = CertificateCredential(tenant_id, client_id, cert_path)

The examples below pass this credential to DataverseClient, opened as a context manager.

Usage

Every operation hangs off a namespace on the client: records for CRUD, query for filtered reads, tables for schema and metadata, dataframe for pandas, files for uploads, and batch for multi-operation requests. Records are plain Python dictionaries keyed by column schema names — custom tables and columns keep their customization prefix (for example "new_"). The sections below cover the capabilities in the order you typically reach for them; each links to the Learn article and runnable sample that go deeper.

Create, read, update, delete

Use the client as a context manager so connections are pooled and cleaned up for you. create returns the new record's GUID as a string, and retrieve returns None when the record does not exist rather than raising on a 404.

from PowerPlatform.Dataverse.client import DataverseClient

with DataverseClient("https://yourorg.crm.dynamics.com", credential) as client:
    account_id = client.records.create("account", {"name": "Contoso Ltd"})   # -> GUID str

    account = client.records.retrieve("account", account_id, select=["name"])
    if account is not None:                       # None when the record is not found
        print(account["name"])

    client.records.update("account", account_id, {"telephone1": "555-0199"})
    client.records.delete("account", account_id)

More on reading and writing records: Work with Dataverse data and the walkthrough.py sample.

Query records

client.query.builder() builds type-safe OData for you — filters use col() with standard Python operators, and it escapes values automatically. Results are iterable and can be handed straight to pandas with .to_dataframe().

from PowerPlatform.Dataverse.models import col

results = (client.query.builder("account")
           .select("name", "revenue")
           .where(col("statecode") == 0)
           .where(col("revenue") > 1_000_000)
           .order_by("revenue", descending=True)
           .top(100)
           .execute())

for account in results:
    print(account["name"], account["revenue"])

df = results.to_dataframe()          # same rows as a pandas DataFrame

col() also supports .in_([...]) and .between(low, high), and expressions compose with &. For large result sets call .page_size(n).execute_pages() to stream page by page, or .expand("primarycontactid") to pull related rows in one request. For raw SQL and FetchXML, see Query data and the sql_examples.py and fetchxml.py samples.

Define and evolve schema

client.tables creates and inspects tables, columns, relationships, and alternate keys. Column types are simple strings ("string", "int", "decimal", "money", "datetime", "bool", "memo"); pass an IntEnum subclass to create a choice column, or a dict spec (e.g. {"type": "memo", "max_length": 2000}) to set constraints like length, range, format, required level, or display name. tables.get returns None when the table does not exist, which makes schema setup idempotent. Every table gets a primary name column automatically — <prefix>_Name unless you pass primary_column — so do not list it in columns.

# Create a custom table with typed columns
if client.tables.get("new_Project") is None:
    client.tables.create("new_Project", {
        "new_Budget": "money",
        "new_StartDate": "datetime",
    }, display_name="Project")

if client.tables.get("new_Task") is None:
    client.tables.create("new_Task", {"new_DueDate": "datetime"}, display_name="Task")

# Add a column to an existing table
client.tables.add_columns("new_Project", {"new_Status": "string"})

# Add a lookup column -- a 1:N relationship from Task to Project
client.tables.create_lookup_field(
    referencing_table="new_task",
    lookup_field_name="new_ProjectId",
    referenced_table="new_project",
    display_name="Project",
)

Columns can carry constraints, be updated in place, and be read back with their type-specific metadata in a single request:

# Create columns with constraints -- pass a dict spec instead of a bare type.
# Keys: max_length, min_value, max_value, precision, format, required, display_name.
client.tables.create("new_Feedback", {
    "new_Rating":  {"type": "int",  "min_value": 1, "max_value": 5},
    "new_Comment": {"type": "memo", "max_length": 2000, "display_name": "Comment"},
})

# Update an existing column's constraints (same override keys as create).
client.tables.update_column("new_Feedback", "new_Comment", {"max_length": 4000})
client.tables.update_columns("new_Feedback", {
    "new_Rating":  {"max_value": 10},
    "new_Comment": {"display_name": "Customer Comment"},
})

# Read a column (or all columns) with typed fields in one request (typed=True).
col = client.tables.get_column("new_Feedback", "new_Comment", typed=True)
print(col["MaxLength"])  # -> 4000
cols = client.tables.list_columns("new_Feedback", typed=True)

Relationship methods take logical names, which are always the schema name in lowercase. For choice columns, many-to-many relationships, and alternate keys, see Customize tables and columns and Manage table relationships.

Work in bulk

Passing a list to create() uses Dataverse's native CreateMultiple; a single change dict applied to a list of IDs uses UpdateMultiple; and deleting a list uses BulkDelete, which returns a job ID and removes the records in the background — pass use_bulk_delete=False to delete them one at a time instead. upsert() creates or updates each record by an alternate key — ideal for idempotent syncs. The key must already exist on the table and its index must have reached Active; create one with client.tables.create_alternate_key and poll client.tables.get_alternate_keys until it does.

from PowerPlatform.Dataverse.models import UpsertItem

# Create many in one request -- returns a list of GUID strings
ids = client.records.create("account", [{"name": "Company A"}, {"name": "Company B"}])

# Apply the same change to every record, then delete them all
client.records.update("account", ids, {"industrycode": 1})
job_id = client.records.delete("account", ids)

# Upsert by alternate key (create if missing, update if present)
client.records.upsert("account", [
    UpsertItem(alternate_key={"accountnumber": "ACC-001"},
               record={"name": "Contoso Ltd", "telephone1": "555-0100"}),
])

For DataFrame-driven loads, client.dataframe.create(table, df) writes an entire pandas DataFrame in one call — see the dataframe_operations.py and alternate_keys_upsert.py samples.

Upload files and group requests

client.files.upload writes a local file to a file column, chunking large files automatically. client.batch packs many operations into one HTTP request, and batch.changeset() groups them so they commit or roll back together.

client.files.upload("account", account_id, "new_Attachment", "report.pdf")

batch = client.batch.new()
batch.records.create("account", {"name": "Company A"})
batch.records.update("account", account_id, {"telephone1": "555-0199"})
result = batch.execute()

See the file_upload.py and batch.py samples.

Use the async client

AsyncDataverseClient (from the [async] extra) exposes the same namespaces and methods as the sync client, each awaitable — so independent operations can run concurrently with asyncio.gather().

import asyncio
from azure.identity.aio import DefaultAzureCredential
from PowerPlatform.Dataverse.aio import AsyncDataverseClient

async def main():
    async with DefaultAzureCredential() as credential, \
               AsyncDataverseClient("https://yourorg.crm.dynamics.com", credential) as client:
        # Independent calls run concurrently
        a_id, b_id = await asyncio.gather(
            client.records.create("account", {"name": "Company A"}),
            client.records.create("account", {"name": "Company B"}),
        )
        accounts = await client.query.builder("account").select("name").top(50).execute()

asyncio.run(main())

See Asynchronous client operations and the examples/aio directory.

Handle errors

The SDK raises a typed exception hierarchy rooted at DataverseError. HttpError exposes status_code and is_transient, so you can retry throttled or transient failures and let everything else fall through to the base class.

from PowerPlatform.Dataverse.core.errors import (
    DataverseError,   # base class -- catch last as a fallback
    ValidationError,  # client-side input validation failed, including unsupported SQL
    MetadataError,    # unknown table / column / relationship
    HttpError,        # Dataverse Web API returned a non-2xx status
)

try:
    client.records.create("account", {"name": "Contoso Ltd"})
except ValidationError as e:
    print(f"Invalid input: {e.message}")
except HttpError as e:
    print(f"HTTP {e.status_code}: {e.message}")
    if e.is_transient:                     # 429 / 502 / 503 / 504
        print(f"Retry after {e.details.get('retry_after')}s")
except DataverseError as e:                # catches MetadataError, etc.
    print(f"Dataverse error: {e.message}")

For retry patterns, timeouts, and HTTP diagnostics logging, see Handle errors and enable HTTP diagnostics.

Documentation

Microsoft Learn is the authoritative reference; the guide for each capability is linked from its section above. New to the SDK? Start here:

Samples

The examples/ directory has complete, runnable scripts for every operation, including advanced scenarios — relationships, batch changesets, file uploads, SQL and FetchXML queries, and DataFrames — plus a full async mirror. Start with the examples guide for a suggested learning progression.

Contributing

This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit Contributor License Agreements.

When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

API Design Guidelines

When contributing new features to this SDK, please follow these guidelines:

  1. Public methods in operation namespaces - New public methods go in the appropriate namespace module under operations/. Public types and constants live in their own modules (e.g., models/metadata.py, common/constants.py)
  2. Add README example for public methods - Add usage examples to this README for public API methods
  3. Document public APIs - Include Sphinx-style docstrings with parameter descriptions and examples for all public methods
  4. Update documentation when adding features - Keep README and SKILL files (note that each skill has 2 copies) in sync
  5. Internal vs public naming - Modules, files, and functions not meant to be part of the public API must use a _ prefix (e.g., _odata.py, _relationships.py). Files without the prefix (e.g., constants.py, metadata.py) are public and importable by SDK consumers

Trademarks

This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies.

Metadata

Release files for PowerPlatform-Dataverse-Client 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for PowerPlatform-Dataverse-Client 1.1.0
File Size Uploaded Provenance
powerplatform_dataverse_client-1.1.0.tar.gz 205.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for PowerPlatform-Dataverse-Client 1.1.0
File Interpreter ABI Platform
powerplatform_dataverse_client-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 441.9 kB

Release files / powerplatform_dataverse_client-1.1.0.tar.gz

Download URL powerplatform_dataverse_client-1.1.0.tar.gz
Size 205.2 kB
Tags Source
SHA-256 checksum
How to use checksums
63577dc6ed5276aef6104cc33914eeb0176f6f818a8f7fa53c9440f99d40463a
BLAKE2b-256 checksum
How to use checksums
a53976baffd655fafb38d1ae608a83a7d6ad7673e6b36b8523b7f6ec3f00a078
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Release files / powerplatform_dataverse_client-1.1.0-py3-none-any.whl

Download URL powerplatform_dataverse_client-1.1.0-py3-none-any.whl
Size 236.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0398a717cd2d235d96498635bf24efda4497808ede3c7c01c0643842b2240297
BLAKE2b-256 checksum
How to use checksums
4b88af03bf74c22148e620b40a229fc54ca6f269ffb8e8da9deb6eb25ce13e60
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Security for PowerPlatform-Dataverse-Client 1.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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A 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.

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