How-to
Automating Reporting WorkflowsDeep dive

Upload an Excel Report to Dropbox with Python

Publish a generated workbook to a Dropbox folder from a scheduled job: a scoped app and a long-lived refresh token instead of expiring access tokens, uploads that overwrite last week's file, chunked upload sessions for large files, a shared link that is reused rather than duplicated, and the errors worth handling.

For teams that live in Dropbox, the most convenient place for a weekly report is a shared Dropbox folder: it syncs to everyone's laptop, keeps version history, and can be shared with people outside the company through a link. The Dropbox Python SDK uploads files in a few lines. The parts that trip up scheduled jobs are elsewhere: access tokens that expire after a few hours, a file that must replace last week's rather than creating report (1).xlsx, the size limit of the simple upload call, and shared links that fail the second time they are created. This page is part of Publishing Excel Reports to Cloud Storage, and covers each.

Prerequisites

Bash
pip install dropbox pandas openpyxl

In the Dropbox App Console, create a scoped app with access to a single folder ("App folder") or to the whole account, and enable the permissions the job needs: files.content.write to upload and sharing.write to create links. The examples here run against a local imitation of the Dropbox API, so the code is exactly what would run in production and the uploaded bytes can be inspected afterwards; weekly_sales.xlsx is the report being published.

One unattended upload A sequence diagram. The job exchanges its refresh token for a short-lived access token, uploads the workbook with overwrite mode, and asks for a shared link, receiving an existing link if one was created before. report job Dropbox OAuth Dropbox API 1. refresh token → access token 2. files/upload (overwrite) 3. FileMetadata with rev and size 4. create or reuse shared link

Step 1 — Authenticate with a refresh token

Dropbox access tokens are short-lived — they expire after a few hours — so a job that stores one works on the day it is set up and fails the next week. Store a refresh token instead, together with the app key and secret, and let the SDK exchange it for access tokens as needed:

Python
import os
import dropbox

dbx = dropbox.Dropbox(
    oauth2_refresh_token=os.environ["DROPBOX_REFRESH_TOKEN"],
    app_key=os.environ["DROPBOX_APP_KEY"],
    app_secret=os.environ["DROPBOX_APP_SECRET"],
)
print(type(dbx).__name__, "ready")

The SDK requests a fresh access token the first time it needs one and again whenever the current one has expired, so a long-running or weekly job never sees an expiry. The three values come from the environment, not the script — see Store Credentials for Excel Report Scripts Safely.

Getting the refresh token is a one-time, interactive step: run the OAuth flow with token_access_type="offline", open the URL it prints, approve, and paste the code back:

Python
from dropbox import DropboxOAuth2FlowNoRedirect

flow = DropboxOAuth2FlowNoRedirect(os.environ["DROPBOX_APP_KEY"], os.environ["DROPBOX_APP_SECRET"],
                                   token_access_type="offline")
print("Open:", flow.start())
result = flow.finish(input("Code: ").strip())
print("refresh token:", result.refresh_token)

Step 2 — Upload, replacing last week's file

files_upload sends the bytes and a path. The mode decides what happens if a file already exists there: WriteMode.overwrite replaces it, which is what a report with a fixed name needs; the default, add, fails or renames:

Python
from pathlib import Path
from dropbox.files import WriteMode

def upload(dbx, local_path, dropbox_path):
    data = Path(local_path).read_bytes()
    meta = dbx.files_upload(data, dropbox_path, mode=WriteMode.overwrite, mute=True)
    return meta

meta = upload(dbx, "weekly_sales.xlsx", "/Reports/Sales/weekly_sales.xlsx")
print(meta.path_display, f"{meta.size:,} bytes", "rev", meta.rev)

mute=True stops Dropbox notifying everyone who shares the folder each week. The SDK computes a content hash of the bytes and sends it with the upload, so Dropbox rejects a file that was corrupted on the way — a check you get without writing anything. Overwriting does not lose history: Dropbox keeps previous versions of the file, and the returned rev identifies this one. Paths are case-insensitive in Dropbox but shown with the case you used, and must start with /.

Step 3 — Large files: upload sessions

The single upload call accepts files up to 150 MB. Larger workbooks — or exports bundled into a zip — go in chunks through an upload session: start it with the first chunk, append the middle chunks at their offsets, and finish with the last chunk and the destination:

Python
from dropbox.files import CommitInfo, UploadSessionCursor

CHUNK = 8 * 1024 * 1024

def upload_large(dbx, local_path, dropbox_path, chunk=CHUNK):
    size = Path(local_path).stat().st_size
    with open(local_path, "rb") as f:
        start = dbx.files_upload_session_start(f.read(chunk))
        cursor = UploadSessionCursor(session_id=start.session_id, offset=f.tell())
        commit = CommitInfo(path=dropbox_path, mode=WriteMode.overwrite, mute=True)
        while size - f.tell() > chunk:
            dbx.files_upload_session_append_v2(f.read(chunk), cursor)
            cursor.offset = f.tell()
        return dbx.files_upload_session_finish(f.read(chunk), cursor, commit)

meta = upload_large(dbx, "weekly_sales.xlsx", "/Reports/Sales/weekly_sales_chunked.xlsx", chunk=2048)
print(meta.path_display, f"{meta.size:,} bytes")

The cursor's offset must equal the number of bytes sent so far, which f.tell() provides exactly. The example uses a 2 KB chunk only to exercise the loop with a small file; in production, chunks of 4–16 MB balance the number of requests against how much has to be resent if one fails. Use the session route for everything above about 100 MB, not just above 150 MB, to leave headroom.

A chunked upload session A timeline of an upload session. It starts with the first chunk and returns a session id, appends further chunks at increasing offsets, and finishes with the last chunk and the commit information naming the destination path. start first chunk → session_id append offset = bytes sent append …as many as needed finish last chunk + CommitInfo(path)

A shared link lets people without access to the folder open the report. Creating a link for a path that already has one, with sharing_create_shared_link_with_settings, raises shared_link_already_exists, which every weekly job hits on its second run. Catch it and fetch the existing link:

Python
from dropbox.exceptions import ApiError

def shared_link(dbx, path):
    try:
        return dbx.sharing_create_shared_link_with_settings(path).url
    except ApiError as err:
        if err.error.is_shared_link_already_exists():
            links = dbx.sharing_list_shared_links(path=path, direct_only=True).links
            return links[0].url
        raise

print("week 1:", shared_link(dbx, "/Reports/Sales/weekly_sales.xlsx"))
print("week 2:", shared_link(dbx, "/Reports/Sales/weekly_sales.xlsx"))
Getting a shared link on every run A decision diagram. The job asks Dropbox to create a shared link. On the first run a new link is returned; on later runs Dropbox answers shared_link_already_exists and the job lists the existing link instead; any other error fails the job. create_shared_link_with_settings first run New link returned directly already exists List existing link direct_only=True other ApiError Fail the job scopes, path, auth

Both runs return the same URL, so a link published once — in a wiki page or an email signature — keeps working as the file is overwritten each week. The link opens a preview page; appending dl=1 instead of dl=0 makes it download the file directly, which suits links in automated emails.

Step 5 — Confirm what arrived

The local imitation keeps the bytes each upload delivered, so the check reads the "uploaded" workbook back with pandas — the same check a real job can do with files_download on a sample of runs:

Python
_, response = dbx.files_download("/Reports/Sales/weekly_sales.xlsx")
check = pd.read_excel(io.BytesIO(response.content))
print(check.shape)

Both the simple and the chunked upload delivered exactly the local file's bytes, and the workbook reads back with its four regions.

Performance notes

A weekly report of a few megabytes uploads in a second or two; the refresh-token exchange adds one short request per run. Upload sessions add a request per chunk, so very small chunks waste time — the 2 KB chunk above is only for demonstration. For many files at once, such as one workbook per region, Dropbox also offers batch commits for upload sessions (files_upload_session_finish_batch_v2), which avoid conflicts between parallel uploads into the same folder; for a handful of files, uploading them one after another is simpler and fast enough. Dropbox applies rate limits per app and user. The client constructor controls how the SDK reacts: max_retries_on_error (4 by default) retries server errors, and max_retries_on_rate_limit governs rate-limit responses — left at its default of None, the SDK keeps retrying them, so a scheduled job may want to set a number and fail loudly instead of waiting indefinitely. The content hash mentioned in Step 2 is the auto_content_hash=True default of the same constructor.

Common pitfalls

  • Storing an access token. It expires in hours; store a refresh token with the app key and secret.
  • The default write mode. add does not replace the existing file; use WriteMode.overwrite for fixed report names.
  • Files over 150 MB with files_upload. Use an upload session.
  • Creating a shared link every run. The second call fails; catch shared_link_already_exists and list the existing link.
  • Missing scopes. An app without files.content.write or sharing.write gets an authorisation error on that call; scopes are set in the App Console, and the refresh token must be re-issued after changing them.

Conclusion

Create a scoped Dropbox app, obtain a refresh token once with the offline OAuth flow, and build the client from the refresh token, app key and secret so the job never sees an expired token. Upload with WriteMode.overwrite and mute=True, switch to upload sessions for large files, create a shared link once and reuse it by catching shared_link_already_exists, and check a sample of uploads by downloading them back.

Frequently asked questions

How do I upload a file to Dropbox with Python? Install the dropbox package, create a client with dropbox.Dropbox(...) and call files_upload(data, "/Folder/file.xlsx", mode=WriteMode.overwrite). The path must start with a slash.

Why does my Dropbox token stop working after a few hours? Dropbox access tokens are short-lived. Use a refresh token obtained with token_access_type="offline", and pass it with the app key and secret to dropbox.Dropbox so the SDK refreshes access tokens automatically.

How do I upload files larger than 150 MB to Dropbox? Use an upload session: files_upload_session_start with the first chunk, files_upload_session_append_v2 for each further chunk with the current offset, and files_upload_session_finish with the last chunk and a CommitInfo naming the destination.

How do I get a Dropbox shared link that stays the same? Create it once with the SDK's create-shared-link call. On later runs, catch the shared_link_already_exists error and read the existing link with sharing_list_shared_links, passing the path and direct_only=True.

More on publishing & sharing files

More on publishing & sharing files →