Skip to content

GitHub Profile Viewer

Abstract

The GitHub REST API is a fantastic teaching API — no key required for basic reads, clean JSON, and data everyone recognizes. In this tutorial you build a Tkinter app that takes a username and shows the user’s name, bio, repo count, and followers. Then you grow it into a real profile dashboard: avatar images, a scrollable repository list sorted by stars, authenticated requests for higher rate limits, and a side-by-side comparison of two developers.

You will leave understanding:

  • How to call a public REST API and read its JSON response.
  • Why data.get("name", "N/A")data.get("name", "N/A") is safer than data["name"]data["name"].
  • How API rate limits work and how authentication raises them.
  • The fetch → display pattern that scales to any JSON API.

Prerequisites

  • Python 3.6 or above.
  • A text editor or IDE.
  • The requestsrequests library: pip install requestspip install requests.
  • Tkinter (bundled with Python).
  • Optional: a GitHub Personal Access Token for higher rate limits.

Getting Started

Create the project

  1. Create a folder named github-profile-viewergithub-profile-viewer.
  2. Inside it, create github_profile_viewer.pygithub_profile_viewer.py.
  3. Install the dependency: pip install requestspip install requests.

Write the code

github_profile_viewer.pySource
github_profile_viewer.py
"""
GitHub Profile Viewer
 
A Python application that fetches and displays GitHub user profile information using the GitHub API.
Features include:
- Fetching user details like name, bio, repositories, followers, etc.
- Displaying the information in a user-friendly format.
"""
 
import requests
from tkinter import Tk, Label, Entry, Button, Text, END, messagebox
 
 
class GitHubProfileViewer:
    def __init__(self, root):
        self.root = root
        self.root.title("GitHub Profile Viewer")
 
        Label(root, text="Enter GitHub Username:").grid(row=0, column=0, padx=10, pady=10)
        self.username_entry = Entry(root, width=30)
        self.username_entry.grid(row=0, column=1, padx=10, pady=10)
 
        Button(root, text="Fetch Profile", command=self.fetch_profile).grid(row=1, column=0, columnspan=2, pady=10)
 
        self.result_text = Text(root, width=50, height=20)
        self.result_text.grid(row=2, column=0, columnspan=2, padx=10, pady=10)
 
    def fetch_profile(self):
        """Fetch GitHub profile information for the entered username."""
        username = self.username_entry.get()
        if not username:
            messagebox.showerror("Error", "Please enter a GitHub username.")
            return
 
        url = f"https://api.github.com/users/{username}"
        response = requests.get(url)
 
        if response.status_code == 200:
            data = response.json()
            self.display_profile(data)
        else:
            messagebox.showerror("Error", f"Failed to fetch profile. Status code: {response.status_code}")
 
    def display_profile(self, data):
        """Display the fetched profile information in the text widget."""
        self.result_text.delete(1.0, END)
        self.result_text.insert(END, f"Name: {data.get('name', 'N/A')}\n")
        self.result_text.insert(END, f"Bio: {data.get('bio', 'N/A')}\n")
        self.result_text.insert(END, f"Public Repos: {data.get('public_repos', 'N/A')}\n")
        self.result_text.insert(END, f"Followers: {data.get('followers', 'N/A')}\n")
        self.result_text.insert(END, f"Following: {data.get('following', 'N/A')}\n")
        self.result_text.insert(END, f"Profile URL: {data.get('html_url', 'N/A')}\n")
 
 
def main():
    root = Tk()
    app = GitHubProfileViewer(root)
    root.mainloop()
 
 
if __name__ == "__main__":
    main()
 
github_profile_viewer.py
"""
GitHub Profile Viewer
 
A Python application that fetches and displays GitHub user profile information using the GitHub API.
Features include:
- Fetching user details like name, bio, repositories, followers, etc.
- Displaying the information in a user-friendly format.
"""
 
import requests
from tkinter import Tk, Label, Entry, Button, Text, END, messagebox
 
 
class GitHubProfileViewer:
    def __init__(self, root):
        self.root = root
        self.root.title("GitHub Profile Viewer")
 
        Label(root, text="Enter GitHub Username:").grid(row=0, column=0, padx=10, pady=10)
        self.username_entry = Entry(root, width=30)
        self.username_entry.grid(row=0, column=1, padx=10, pady=10)
 
        Button(root, text="Fetch Profile", command=self.fetch_profile).grid(row=1, column=0, columnspan=2, pady=10)
 
        self.result_text = Text(root, width=50, height=20)
        self.result_text.grid(row=2, column=0, columnspan=2, padx=10, pady=10)
 
    def fetch_profile(self):
        """Fetch GitHub profile information for the entered username."""
        username = self.username_entry.get()
        if not username:
            messagebox.showerror("Error", "Please enter a GitHub username.")
            return
 
        url = f"https://api.github.com/users/{username}"
        response = requests.get(url)
 
        if response.status_code == 200:
            data = response.json()
            self.display_profile(data)
        else:
            messagebox.showerror("Error", f"Failed to fetch profile. Status code: {response.status_code}")
 
    def display_profile(self, data):
        """Display the fetched profile information in the text widget."""
        self.result_text.delete(1.0, END)
        self.result_text.insert(END, f"Name: {data.get('name', 'N/A')}\n")
        self.result_text.insert(END, f"Bio: {data.get('bio', 'N/A')}\n")
        self.result_text.insert(END, f"Public Repos: {data.get('public_repos', 'N/A')}\n")
        self.result_text.insert(END, f"Followers: {data.get('followers', 'N/A')}\n")
        self.result_text.insert(END, f"Following: {data.get('following', 'N/A')}\n")
        self.result_text.insert(END, f"Profile URL: {data.get('html_url', 'N/A')}\n")
 
 
def main():
    root = Tk()
    app = GitHubProfileViewer(root)
    root.mainloop()
 
 
if __name__ == "__main__":
    main()
 

Run it

command
C:\Users\Your Name\github-profile-viewer> python github_profile_viewer.py
# Type a username (e.g. "torvalds") and click "Fetch Profile".
command
C:\Users\Your Name\github-profile-viewer> python github_profile_viewer.py
# Type a username (e.g. "torvalds") and click "Fetch Profile".

Step-by-Step Explanation

1. The endpoint

github_profile_viewer.py
url = f"https://api.github.com/users/{username}"
response = requests.get(url)
github_profile_viewer.py
url = f"https://api.github.com/users/{username}"
response = requests.get(url)

GitHub’s user endpoint needs no key for basic data. The username goes straight into the path. (In production you’d URL-encode it, but GitHub usernames are already restricted to safe characters.)

2. Check the status code

github_profile_viewer.py
if response.status_code == 200:
    data = response.json()
    self.display_profile(data)
else:
    messagebox.showerror("Error", f"Failed to fetch profile. Status code: {response.status_code}")
github_profile_viewer.py
if response.status_code == 200:
    data = response.json()
    self.display_profile(data)
else:
    messagebox.showerror("Error", f"Failed to fetch profile. Status code: {response.status_code}")

404404 means no such user, 403403 usually means you hit the rate limit. Reporting the code turns a silent failure into a debuggable one.

3. Safe field access

github_profile_viewer.py
self.result_text.insert(END, f"Name: {data.get('name', 'N/A')}\n")
self.result_text.insert(END, f"Bio: {data.get('bio', 'N/A')}\n")
github_profile_viewer.py
self.result_text.insert(END, f"Name: {data.get('name', 'N/A')}\n")
self.result_text.insert(END, f"Bio: {data.get('bio', 'N/A')}\n")

Many profiles leave namename or biobio empty, returning nullnull. data.get('name', 'N/A')data.get('name', 'N/A') substitutes a default instead of printing NoneNone or raising KeyErrorKeyError. Use .get().get() for every optional field.

4. The Text widget

github_profile_viewer.py
self.result_text.delete(1.0, END)     # clear previous result
self.result_text.insert(END, ...)     # append lines
github_profile_viewer.py
self.result_text.delete(1.0, END)     # clear previous result
self.result_text.insert(END, ...)     # append lines

1.01.0 means “line 1, character 0”. Clearing before each fetch prevents profiles from stacking on top of each other.

Beat the Rate Limit with Authentication

Unauthenticated requests are capped at 60/hour; authenticated ones get 5,000/hour. Add a token:

auth.py
import os, requests
 
headers = {"Authorization": f"token {os.environ['GITHUB_TOKEN']}"}
r = requests.get("https://api.github.com/users/torvalds", headers=headers, timeout=10)
auth.py
import os, requests
 
headers = {"Authorization": f"token {os.environ['GITHUB_TOKEN']}"}
r = requests.get("https://api.github.com/users/torvalds", headers=headers, timeout=10)

Create a token at GitHub → Settings → Developer settings → Personal access tokens. Store it in an environment variable, never in the code.

Show the Avatar

The JSON includes avatar_urlavatar_url:

avatar.py
from io import BytesIO
import requests
from PIL import Image, ImageTk     # pip install pillow
 
def load_avatar(url):
    raw = requests.get(url, timeout=10).content
    img = Image.open(BytesIO(raw)).resize((96, 96))
    return ImageTk.PhotoImage(img)
avatar.py
from io import BytesIO
import requests
from PIL import Image, ImageTk     # pip install pillow
 
def load_avatar(url):
    raw = requests.get(url, timeout=10).content
    img = Image.open(BytesIO(raw)).resize((96, 96))
    return ImageTk.PhotoImage(img)

Display it in a Label(image=...)Label(image=...) — remember to keep a reference or Tkinter garbage-collects the image.

List the User’s Repositories

A second endpoint returns repos; sort them by stars:

repos.py
def top_repos(username, n=5):
    r = requests.get(f"https://api.github.com/users/{username}/repos",
                     params={"sort": "updated", "per_page": 100}, timeout=10)
    repos = r.json()
    repos.sort(key=lambda x: x["stargazers_count"], reverse=True)
    return [(x["name"], x["stargazers_count"], x["language"]) for x in repos[:n]]
repos.py
def top_repos(username, n=5):
    r = requests.get(f"https://api.github.com/users/{username}/repos",
                     params={"sort": "updated", "per_page": 100}, timeout=10)
    repos = r.json()
    repos.sort(key=lambda x: x["stargazers_count"], reverse=True)
    return [(x["name"], x["stargazers_count"], x["language"]) for x in repos[:n]]

Compare Two Developers

Fetch two profiles and lay them out side by side — followers, repos, and total stars. A great use of the API for “who’s more active” debates and a clean way to practice reusing your fetch function.

Common Mistakes

ProblemCauseFix
403 Forbidden403 Forbidden after a few callsHit the 60/hour anonymous limitAuthenticate with a token (5,000/hour)
Prints NoneNone for name/bioField was nullnullUse data.get(field, "N/A")data.get(field, "N/A")
Profiles stack up in the boxDidn’t clear the TextText widgetdelete(1.0, END)delete(1.0, END) before inserting
Avatar doesn’t showImage garbage-collectedKeep a reference: self.photo = ...self.photo = ...
App hangs on slow networkNo timeouttimeoutAdd timeout=10timeout=10 to every request
Crash on empty usernameNo input guardCheck if not usernameif not username and return early

Variations to Try

  1. Repo dashboard — table of repos with stars, forks, and language.
  2. Contribution stats — total stars across all repos, most-used language.
  3. Org viewer — switch the endpoint to /orgs/{name}/orgs/{name}.
  4. Follower explorer — list followers and click through to their profiles.
  5. Export — save a profile summary to JSON or a PDF card.
  6. Search — use /search/users/search/users to find profiles by query.
  7. Dark mode — theme the UI; show language colors like GitHub does.

Real-World Applications

  • Recruiting tools — quick candidate snapshots from a username.
  • Developer dashboards — track your own or your team’s activity.
  • OSS analytics — measure project reach and contributor influence.
  • Portfolio sites — auto-populate a “my GitHub” section.

Educational Value

  • REST APIs — endpoints, paths, JSON, status codes.
  • Rate limits & auth — tokens, headers, and being a good API citizen.
  • Defensive coding.get().get() defaults and status checks.
  • Working with images — fetching and rendering remote bytes in a GUI.

Next Steps

  • Add token authentication to lift the rate limit.
  • Render the avatar and a top-repos list.
  • Build the side-by-side comparison view.
  • Export profiles to JSON.

Conclusion

You built a GitHub profile viewer from one requests.getrequests.get, then grew it into a dashboard with avatars, repo rankings, authenticated requests, and comparisons. The fetch → check status → safe-parse → display loop you practiced here is the exact shape of every API client you’ll write — only the endpoint changes. Full source on GitHub. Find more API projects on Python Central Hub.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did