Back to Blog
Python

Python PyGithub: Commits, Releases, and Repository Contents

python pygithub commits releases and repository contents: Use PyGithub to fetch commits, releases, and repository contents from the GitHub API, with pagination, rate-l...

PyGithubGitHub APIPythonGitAutomation
Illustration of PyGithub retrieving commits, releases, and files from a GitHub repository.

PyGithub is the most widely used Python client for the GitHub REST API. When you need commit history, file contents, or release metadata from a repository, the Repository object exposes all three through the same authenticated session. This article covers python pygithub commits releases and repository contents: the practical patterns for fetching each data set, handling pagination, and combining them in a single script.

Authentication and Repository Access

Every PyGithub operation starts with a Github instance and a Repository handle. GitHub removed password authentication for the REST API, so a personal access token or a GitHub App token is required for anything beyond anonymous access.

from github import Github, Auth auth = Auth.Token("github_pat_...") g = Github(auth=auth) repo = g.get_repo("owner/repository")

The Auth.Token class is the current recommended way to pass credentials. If you work against GitHub Enterprise, pass the server URL as well:

g = Github(auth=auth, base_url="https://github.example.com/api/v3")

Anonymous access is possible with Github() and no auth, but the unauthenticated rate limit is a small fraction of the authenticated one, so any script that iterates over commits or releases should use a token. Keep the token out of source control and read it from an environment variable.

Reading Repository Contents

repo.get_contents(path) is the entry point for file and directory access. For a file it returns a single ContentFile; for a directory it returns a list of ContentFile objects.

root = repo.get_contents("") for item in root: print(item.type, item.path)

Each item has a type of "file" or "dir". To read a file's raw bytes, use decoded_content:

readme = repo.get_contents("README.md") text = readme.decoded_content.decode("utf-8") print(text[:500])

decoded_content returns bytes, so decode with the file's actual encoding. For a file at a specific tag or branch, pass the ref parameter:

content = repo.get_contents("version.txt", ref="v1.2.0")

Two limitations matter in practice. First, get_contents makes one API request per path, so walking a large tree recursively generates many requests. For a full snapshot, the Git trees API (repo.get_git_tree(sha, recursive=True)) is more efficient. Second, the contents API has a size limit for individual files; very large files should be fetched through the raw URL or the Git blobs API instead.

Working with Commits

repo.get_commits() returns a PaginatedList of commit objects, ordered newest first. You can filter by path, author, or starting ref.

commits = repo.get_commits(path="src/") for commit in commits: print(commit.sha, commit.commit.message.splitlines()[0])

The path argument limits results to commits that touched that path. The sha argument accepts a branch name or commit SHA and starts the listing from that ref; a tag name resolves as a ref in most cases.

Useful fields on a commit object:

for commit in commits: author = commit.commit.author print(commit.sha) print(author.name, author.date) print(commit.commit.message) print(commit.stats.additions, commit.stats.deletions)

commit.files lists the files changed in that commit, but only when the commit was loaded with file detail. On the default listing, PyGithub may leave this list empty; call repo.get_commit(commit.sha) to fetch the full detail for a single commit.

detail = repo.get_commit(commit.sha) for f in detail.files: print(f.filename, f.status, f.additions, f.deletions)

Working with Releases

repo.get_releases() returns published releases, newest first. Each Release exposes the tag, title, body, and publication date.

releases = repo.get_releases() for release in releases: print(release.tag_name, release.name, release.published_at) print(release.body)

Release assets are not loaded automatically. Call release.get_assets() to enumerate them:

for release in releases: for asset in release.get_assets(): print(asset.name, asset.size, asset.browser_download_url)

For the most recent release, repo.get_latest_release() is more direct. It raises GithubException when the repository has no releases, so guard the call:

try: latest = repo.get_latest_release() except GithubException: latest = None

To fetch a release by tag, use repo.get_release("v1.2.0"), which also raises if the tag does not exist.

Pagination and Rate Limits

Every list-returning method in PyGithub returns a PaginatedList, which loads results lazily in pages of 30 by default. Iterating it issues additional requests as needed. This matters for two reasons: memory and rate limits.

commits = repo.get_commits() print(commits.totalCount) for page_index in range(commits.totalCount // 30 + 1): page = commits.get_page(page_index) for commit in page: print(commit.sha)

get_page(n) fetches page n explicitly, which is useful when you only need a range of results. You can also reduce request count by raising per_page:

commits = repo.get_commits(per_page=100)

Check the remaining quota before a long loop:

core = g.get_rate_limit().core print(core.remaining, core.limit, core.reset)

When the limit is exhausted, PyGithub raises RateLimitExceededException. For batch jobs, sleep until core.reset and retry, or structure the loop to stop once core.remaining is low.

A Combined Example: Release Notes from File Changes

A common task is generating release notes from the commits and changed files between two tags. The following script combines all three capabilities: it reads a version file, lists releases, and walks commits since the latest tag.

import os from github import Github, Auth, GithubException auth = Auth.Token(os.environ["GITHUB_TOKEN"]) g = Github(auth=auth) repo = g.get_repo("owner/repository") try: latest = repo.get_latest_release() except GithubException: print("No releases found") raise SystemExit(1) print(f"Latest release: {latest.tag_name}") version_file = repo.get_contents("version.txt", ref=latest.tag_name) print("Version at release:", version_file.decoded_content.decode().strip()) commits = repo.get_commits(sha=latest.tag_name, per_page=100) for commit in commits: first_line = commit.commit.message.splitlines()[0] print(f"{commit.sha[:8]} {first_line}")

This pattern is useful for changelog generation, CI pipelines that need to know what changed since the last release, and tools that mirror release artifacts. Note that get_commits(sha=latest.tag_name) starts from the tag ref; if the tag points to a commit that is not an ancestor of the default branch, filter results accordingly.

Handling Missing Data and Large Repositories

Several edge cases produce surprising failures. A repository with no commits raises GithubException on get_commits(). A path that does not exist raises UnknownObjectException, a subclass of GithubException. Empty directories are not returned by the contents API because Git does not track them.

For large repositories, prefer the trees API for full snapshots and the blobs API for individual large files, since both are designed for bulk reads. The contents API remains the right tool for targeted file access, especially when you need decoded_content or a specific ref.

When you combine commits, releases, and contents in one script, keep the pagination and rate-limit behavior in mind: each list iteration consumes quota, and a long loop over a large repository can exhaust the hourly allowance. Reading g.get_rate_limit() before and during the loop gives you a way to stop cleanly instead of failing mid-run.

python pygithub commits releases and repository contents: Pr | RYUSLOG DEV