google-api-python-client
One Python package that talks to every Google API that publishes a discovery document: Drive, Gmail, Calendar, Sheets, YouTube, Admin SDK, Cloud Compute and hundreds more. You call build('drive', 'v3', credentials=creds) and get back a Resource object whose methods are generated at runtime from a JSON description of the API, so drive.files().list(pageSize=10).execute() works without any Drive-specific code in the library. Since 2.0 those discovery documents ship inside the package rather than being downloaded at build() time, which made startup reliable and pushed the install past 50 MB. Google states plainly in the README that the library is complete and in maintenance mode: critical bugs and security issues get fixed, new features do not land, and for new code the maintainers point you at the per-service Cloud Client Libraries instead.
The only practical way to reach most Google Workspace APIs from Python, and it works fine once credentials are sorted. Treat it as frozen infrastructure: no new features are coming, it is synchronous and untyped, and anything on Google Cloud proper belongs in a Cloud Client Library instead.
Use it if
- You need a Google API that has no dedicated Cloud Client Library, which covers most of Workspace: Gmail, Calendar, Drive, Sheets, Docs, Admin SDK, YouTube Data
- You call several different Google APIs from one service and want a single dependency and a single auth story rather than one package per API
- You want batch requests, where up to 1000 calls go out in one HTTP round trip through new_batch_http_request()
- You need resumable uploads or chunked downloads for large Drive or YouTube media without writing the resumable protocol yourself
- You are writing new code against a Google Cloud service (Storage, BigQuery, Pub/Sub, Firestore). The maintainers themselves recommend the Cloud Client Libraries, which are per-service, better typed, and actively getting features
- You care about install size. The cached discovery documents push the package past 50 MB, which is painful for Lambda layers, slim containers and cold starts
- You run multi-threaded code. It sits on httplib2, which is not thread-safe, so every thread needs its own httplib2.Http() instance passed through a custom requestBuilder or per-call http argument. Sharing one service object across threads produces corrupted responses that look like random API errors
- You need asyncio. Everything here is blocking, there is no async variant, and running it under an event loop means pushing calls into a thread pool
- You want editor autocomplete or type checking. Methods do not exist until runtime, so your IDE shows nothing and mypy sees Any everywhere unless you add the third-party google-api-python-client-stubs package
- You only need one thing, like reading a spreadsheet. gspread does that in three lines against the same API
Setup reality
pip install google-api-python-client brings httplib2, uritemplate, google-auth, google-auth-httplib2 and google-api-core, and the roughly 15 MB wheel unpacks to more than 50 MB on disk because every discovery document is bundled. The install is the easy part. Credentials are where the time goes: API keys for public data, an installed-app OAuth flow with a client_secret.json and a locally cached token for user data, a service account for server-to-server, and domain-wide delegation on top of that if you are impersonating Workspace users. Getting a 403 usually means you forgot to enable the API in the Cloud Console project, and scope changes require deleting the cached token before the new scope takes effect. Two more traps: pass static_discovery=True (or leave it at the default with credentials supplied) so build() does not try a network call, and pass cache_discovery=False if you see the noisy file_cache is only supported with oauth2client warning. Finally, if you multi-thread anything, wire up the per-thread Http() pattern from day one instead of retrofitting it after the first mystery failure.
Patterns
Build a service client with an API keybuild-service-api-key
from googleapiclient.discovery import build
youtube = build("youtube", "v3", developerKey=API_KEY, static_discovery=True)
response = (
youtube.search()
.list(q="python", part="snippet", maxResults=5)
.execute()
)
for item in response["items"]:
print(item["snippet"]["title"])API keys only reach public data. static_discovery=True guarantees the bundled discovery document is used and no network call happens during build().
Authenticate as a service accountservice-account-auth
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = ["https://www.googleapis.com/auth/drive.readonly"]
creds = service_account.Credentials.from_service_account_file(
"service-account.json", scopes=SCOPES
)
# Workspace domain-wide delegation: act as a real user
creds = creds.with_subject("user@example.com")
drive = build("drive", "v3", credentials=creds)A service account has its own empty Drive. Without with_subject() and domain-wide delegation configured by an admin, files().list() returns nothing rather than an error.
Run the installed-app OAuth flow with a cached tokenoauth-installed-app
import os.path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"client_secret.json", SCOPES
)
creds = flow.run_local_server(port=0)
with open("token.json", "w") as fh:
fh.write(creds.to_json())
gmail = build("gmail", "v1", credentials=creds)google-auth-oauthlib is a separate install. If you change SCOPES you must delete token.json, otherwise the cached token keeps the old scope and calls fail with 403 insufficient permissions.
Page through a large listpaginate-results
files = drive.files()
request = files.list(pageSize=100, fields="nextPageToken, files(id, name)")
while request is not None:
response = request.execute()
for f in response.get("files", []):
print(f["id"], f["name"])
request = files.list_next(request, response)Call execute() once per loop iteration. list_next() returns None when there are no more pages, and it needs nextPageToken to be present in your fields mask.
Ask for only the fields you needpartial-response-fields
response = (
drive.files()
.list(q="mimeType='application/pdf'", fields="files(id,name,size)")
.execute()
)The server returns the full resource by default. A fields mask cuts response size and often makes the difference on rate-limited APIs; slashes nest, commas separate.
Handle API errors properlyhandle-http-errors
import json
from googleapiclient.errors import HttpError
try:
drive.files().get(fileId="missing").execute()
except HttpError as error:
status = error.resp.status
detail = json.loads(error.content).get("error", {})
if status == 404:
print("not found")
elif status in (403, 429) and detail.get("errors", [{}])[0].get("reason") == "rateLimitExceeded":
print("back off and retry")
else:
raiseA 403 from Google means either a quota problem or a permission problem, and only the reason field inside error.content tells you which.
Upload a large file in chunksresumable-upload
from googleapiclient.http import MediaFileUpload
media = MediaFileUpload(
"report.pdf", mimetype="application/pdf", chunksize=1024 * 1024, resumable=True
)
request = drive.files().create(
body={"name": "report.pdf"}, media_body=media, fields="id"
)
response = None
while response is None:
status, response = request.next_chunk()
if status:
print(f"uploaded {int(status.progress() * 100)}%")
print(response["id"])Chunks for files over 256 KB must be a multiple of 256 KB. Media uploads cannot be put inside a batch request.
Download a file to memory or diskdownload-media
import io
from googleapiclient.http import MediaIoBaseDownload
request = drive.files().get_media(fileId=file_id)
buffer = io.BytesIO()
downloader = MediaIoBaseDownload(buffer, request)
done = False
while not done:
status, done = downloader.next_chunk()
print(f"{int(status.progress() * 100)}%")
with open("out.bin", "wb") as fh:
fh.write(buffer.getvalue())get_media() only works for binary files. Google Docs, Sheets and Slides need files().export_media() with a target mimeType instead.
Send many calls in one HTTP requestbatch-requests
def on_response(request_id, response, exception):
if exception is not None:
print(request_id, "failed:", exception)
else:
print(request_id, response["name"])
batch = drive.new_batch_http_request(callback=on_response)
for i, file_id in enumerate(file_ids):
batch.add(drive.files().get(fileId=file_id), request_id=str(i))
batch.execute()Maximum 1000 calls per batch, and execute() blocks until every callback has run. A failed sub-request does not fail the batch, so you must inspect the exception argument.
Use the client from multiple threadsthread-safe-clients
import httplib2
import google_auth_httplib2
import googleapiclient.http
from googleapiclient import discovery
def build_request(http, *args, **kwargs):
new_http = google_auth_httplib2.AuthorizedHttp(creds, http=httplib2.Http())
return googleapiclient.http.HttpRequest(new_http, *args, **kwargs)
authorized_http = google_auth_httplib2.AuthorizedHttp(creds, http=httplib2.Http())
service = discovery.build(
"drive", "v3", requestBuilder=build_request, http=authorized_http
)httplib2.Http() is not thread-safe. Without this pattern, concurrent execute() calls on one service object interleave on the same socket and return each other's data.
Silence the file_cache discovery warningsilence-cache-warning
service = build(
"sheets", "v4", credentials=creds, cache_discovery=False
)The file_cache is only supported with oauth2client<4.0.0 warning is harmless but fills logs; since discovery documents are bundled, disabling the cache costs nothing.
Get autocomplete and type checkingadd-type-stubs
# pip install google-api-python-client-stubs
from googleapiclient.discovery import build
# mypy and Pyright now resolve the resource chain:
drive = build("drive", "v3", credentials=creds)
result = drive.files().list(pageSize=10).execute()Stubs are a separate community package, not shipped by Google, and they only cover the more popular APIs; anything else stays Any.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| google-cloud-storage | PyPI | An example of the per-service Cloud Client Libraries the maintainers recommend for new Google Cloud code |
| gspread | PyPI | You only touch Google Sheets and want a small API instead of raw Sheets v4 request bodies |
| pydrive2 | PyPI | Drive-only work where you want file objects and a simpler auth wrapper over this same client |