GitHub Repository Health Analyzer

Build a Python CLI tool that scores GitHub repos by health metrics.

Introduction

30 Second Summary

Every engineering team has repositories that haven't been touched in months. When someone asks which projects are healthy and which need attention, the answer is usually buried across dozens of GitHub pages.

In this project, you will build a Python command-line tool that connects to the GitHub API, pulls live repository data for any organization, and generates a health report right in your terminal.

What You'll Build

Run your finished tool against any GitHub organization and watch it pull live data, crunch health scores, and print a sorted report showing which repos are thriving and which need attention.

By the end of this project, you'll have:

  • A Python CLI tool that pulls live data from the GitHub REST API for any organization and scores each repository's health from 0 to 100.
  • A formatted terminal report with aligned columns, Healthy/Warning/Critical status labels, and summary statistics you can show to any stakeholder.
  • Graceful error handling that catches misspelled organization names, rate limits, and network failures with clear, friendly messages instead of crashes.
  • Secret Mission: Add a deep dive mode that fetches the language breakdown for each repository and displays the top three languages as percentages in the report.

Are there any prerequisites?

This project assumes you know basic Python: variables, loops, and functions. You do not need any prior experience with APIs or web requests.

Before We Start

Before writing any code, take a moment to lock in what you're about to build and why it matters.

Set Up Your Project and Install Dependencies

Your health analyzer needs to talk to GitHub's servers to pull repository data. Python has built-in tools for web requests, but a library called requests makes HTTP communication much simpler.

In this step, you'll create a project folder, install requests, and write a quick test to make sure everything connects.

In this step, get ready to:
  • Create the repo-health project folder and open it in VS Code.
  • Install the requests library.
  • Create analyzer.py and verify your setup.
Create your project folder and open it in VS Code

Every project starts with a dedicated folder to keep your files organized. You'll create one on your Desktop so it's easy to find.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir repo-health
cd repo-health
  • Confirm you're in the right place by running:
pwd

You should see a path ending in repo-health.

  • Open this folder in VS Code by running:
code .

VS Code opens with the repo-health folder loaded in the Explorer sidebar on the left.

Command not recognized?

On macOS, open VS Code manually, press Cmd+Shift+P, type Shell Command: Install 'code' command in PATH, select it, and restart your terminal.

On Windows, reinstall VS Code and make sure Add to PATH is checked during setup. Restart your terminal after reinstalling.

As a workaround, open VS Code through your search bar, click File, then Open Folder, and select the repo-health folder on your Desktop.

Still stuck? Help me fix the code . command so I can open VS Code from the terminal.

Install the requests library

The requests library is the only external dependency your project needs. It handles all the HTTP communication with GitHub's REST API.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (the backtick key, next to the number 1). This works on both macOS and Windows.
  • Install requests by running this command:
pip install requests==2.34.2

You should see output ending with a line that includes Successfully installed requests-2.34.2.

Install command failed?

  • If you see pip: command not found, try pip3 install requests==2.34.2 instead. Some systems use pip3 for Python 3.
  • If you see a permissions error, add --user to the end of the command to install for your user only.
  • If you see a network error, check your internet connection and try again.

Still stuck? Help me install the requests library with pip. I'm getting an error.

Create the main script file and verify everything works

Now you'll create the Python file that will hold all your analyzer code. A quick two-line test will confirm that requests is installed and importable.

  • In the VS Code Explorer sidebar on the left, hover over the REPO-HEALTH header.
  • Click the New File icon (a page with a plus sign).
  • Type analyzer.py and press Enter.
  • Copy the following code into analyzer.py:
import requests
print(requests.__version__)

What does this code do?

  • import requests loads the library you just installed, making its functions available in your script.
  • print(requests.__version__) prints the installed version number. If this runs without errors, your library is correctly installed and importable.
  • Save analyzer.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).

Before you run this script, what do you think you'll see printed in the terminal?

  • Run your script in the VS Code terminal:
python3 analyzer.py

You should see 2.34.2 printed on a single line with no errors. That confirms Python can find and load the requests library.

✔️ Awesome, I've got everything!

Great. Double check you've saved analyzer.py and you're ready for the next step.

ⓧ I'd like to double check the full code

Here's everything that should be in analyzer.py right now:

import requests
print(requests.__version__)

Not seeing the version number?

  • Make sure you saved the file before running it. Unsaved changes won't be picked up by Python.
  • If you see ModuleNotFoundError: No module named 'requests', the library did not install correctly. Re-run the pip install command from the previous substep.
  • On Windows, if python3 is not recognized, try python analyzer.py instead. Some Windows installs use python without the 3.

Still stuck? Help me run analyzer.py. I'm not seeing the expected version number output.

Your project is set up and ready to go. Next up, you'll write your first API call to fetch live repository data from GitHub.

Fetch Live Data from the GitHub API

Your project folder is ready and requests is installed. Now it is time to use it.

Before you can analyze anything, you need real data to work with. In this step, you will write a function that calls the GitHub REST API, pulls live repository information, and prints it to the terminal.

In this step, get ready to:
  • Write a function that fetches repository data from the GitHub API.
  • Print live repo names and star counts to the terminal.
  • Add error handling so the script exits gracefully on bad API responses.
Write a function to fetch repository data

GitHub exposes a REST API that anyone can call without signing up or creating an API key. You will use the list organization repositories endpoint to grab data for every public repo in an organization.

  • Switch back to analyzer.py in VS Code.
  • Select all the existing code and delete it.
  • Replace it with the following code:
import requests


def fetch_repos(org):
    """Fetch repositories for a GitHub organization."""
    # Build the API URL for the organization's repos
    url = f"https://api.github.com/orgs/{org}/repos"
    headers = {"User-Agent": "repo-health-analyzer"}
    params = {"per_page": 100}

    # Make the API request
    response = requests.get(url, headers=headers, params=params, timeout=10)

    # Parse the JSON response into a Python list
    repos = response.json()
    return repos


# Test with Netflix's public repositories
org = "netflix"
repos = fetch_repos(org)
for repo in repos:
    print(repo["name"], repo["stargazers_count"])

What does this code do?

  • fetch_repos(org) builds a URL for the GitHub endpoint GET /orgs/{org}/repos. The org parameter is the GitHub organization name, like "netflix".
  • The User-Agent header is required by GitHub for all API requests. Without it, GitHub may reject your request.
  • per_page=100 tells GitHub to return up to 100 repositories per response (the maximum allowed).
  • response.json() converts the raw JSON text from GitHub into a Python list of dictionaries. Each dictionary represents one repository.

Before you run this, what do you think the output will look like? Will you see full JSON objects, or just names and numbers?

  • Save analyzer.py.
  • Run the script in your terminal:
python3 analyzer.py

You should see a list of repository names followed by their star counts, one per line. These are live repos from Netflix's GitHub organization.

Not seeing any output?

  • Make sure you saved analyzer.py before running the command.
  • Check that your terminal is inside the repo-health folder. You can confirm by running pwd.
  • If you see a connection error, check that you are connected to the internet.

Still stuck? Ask for help.

What is a REST API?

A REST API is a way for programs to request data from a server using standard HTTP requests. You send a URL to a server, and it sends back structured data (usually JSON).

Your script just made the same kind of request your browser makes when you visit a website. The difference is that instead of getting a web page, you got raw data you can process with Python.

Add error handling for API responses

Your function works when the organization exists. But what if someone types the name wrong, or GitHub blocks the request? Right now the script would crash with an ugly traceback.

The GitHub API returns HTTP status codes that tell you whether the request succeeded or failed. You will check these codes and print a clear message for each failure.

  • In analyzer.py, find this line at the very top of the file:
import requests
  • Add import sys on a new line above it, so the top of your file looks like this:
import sys
import requests
  • Now scroll down inside the fetch_repos function and find this line:
    response = requests.get(url, headers=headers, params=params, timeout=10)
  • Add the following error handling code directly below that line, replacing the existing repos = response.json() and return repos lines:
    # Check for common API errors
    if response.status_code == 404:
        print(f"Organization '{org}' not found. Check the spelling and try again.")
        sys.exit(1)
    elif response.status_code == 403:
        print("Rate limit exceeded. GitHub allows 60 unauthenticated requests per hour. Try again later.")
        sys.exit(1)
    elif response.status_code != 200:
        print(f"GitHub API error: {response.status_code}")
        sys.exit(1)

    repos = response.json()
    return repos

What does this code do?

  • A 404 status code means the organization does not exist. This catches typos in the org name.
  • A 403 status code means GitHub is blocking the request. For unauthenticated requests, this happens when you exceed 60 requests per hour.
  • sys.exit(1) stops the script immediately with an error code. This prevents the script from crashing with confusing traceback text.
  • Save analyzer.py.

Time to test the error handling. What do you think will happen if you point the script at an organization that does not exist?

  • In analyzer.py, find this line near the bottom of the file:
org = "netflix"
  • Change "netflix" to "nonexistent-org-12345" so the line looks like this:
org = "nonexistent-org-12345"
  • Save the file and run the script:
python3 analyzer.py

Instead of a crash, you should see a clean message: Organization 'nonexistent-org-12345' not found. Check the spelling and try again.

  • Now change the org back to "netflix" so the line reads:
org = "netflix"
  • Save the file and run the script one more time:
python3 analyzer.py

You should see the full list of Netflix repository names and star counts again, confirming the happy path still works.

Still seeing a traceback instead of the error message?

  • Make sure import sys is at the very top of your file, above import requests.
  • Check that the if/elif block is indented inside the fetch_repos function (four spaces in from the left).
  • Make sure the error handling block comes after the response = requests.get(...) line and before repos = response.json().

Need more help? Ask here.

✔️ Awesome, I've got everything!

Great. Double check you have saved analyzer.py and the org is set back to "netflix".

ⓧ I'd like to double check the full code

Here is the complete analyzer.py at the end of this step. Compare it with yours line by line.

import sys
import requests


def fetch_repos(org):
    """Fetch repositories for a GitHub organization."""
    url = f"https://api.github.com/orgs/{org}/repos"
    headers = {"User-Agent": "repo-health-analyzer"}
    params = {"per_page": 100}

    response = requests.get(url, headers=headers, params=params, timeout=10)

    if response.status_code == 404:
        print(f"Organization '{org}' not found. Check the spelling and try again.")
        sys.exit(1)
    elif response.status_code == 403:
        print("Rate limit exceeded. GitHub allows 60 unauthenticated requests per hour. Try again later.")
        sys.exit(1)
    elif response.status_code != 200:
        print(f"GitHub API error: {response.status_code}")
        sys.exit(1)

    repos = response.json()
    return repos


org = "netflix"
repos = fetch_repos(org)
for repo in repos:
    print(repo["name"], repo["stargazers_count"])

You have a working connection to a live API and real data flowing into your script. Next up, you will turn that wall of names and numbers into a health score that tells you which repos are thriving and which need attention.

Transform Raw Data into Health Scores

In the last step, you pulled live repository data from the GitHub REST API. Your terminal is showing names and star counts straight from Netflix's public repos.

Your customer does not care about raw star counts. They want a quick answer: which repos are healthy and which are dying? In this step, you will build a scoring function that condenses multiple data points into a single health score between 0 and 100.

In this step, get ready to:
  • Calculate a health score (0-100) for each repository based on stars, forks, recency, and issues.
  • Sort repositories from healthiest to least healthy.
Write the health scoring function

Each repo gets four measurements combined into a single 0-100 score. Stars and forks measure community interest. Recency measures whether the project is still actively developed. The issue ratio measures how well the project is maintained.

How the scoring weights work

The four categories add up to a maximum of 100 points. Stars contribute up to 30 points, forks up to 20, recency up to 30, and issue ratio up to 20. Recency is weighted equally to stars because an actively maintained repo is healthier than a popular but abandoned one.

Each repo's star and fork scores are relative to the highest in the organization. A repo with the most stars always gets the full 30 points. This means scores reflect standing within the org, not an absolute measure.

The scoring function needs timestamps to calculate how recently each repo was updated. That requires Python's datetime module.

  • In analyzer.py, find this line at the top of your file:
import requests
  • Add this new import directly below it:
from datetime import datetime, timezone
  • Start the calculate_health function by copying the following code below the fetch_repos function (below its return repos line):
def calculate_health(repos):
    """Calculate a health score (0-100) for each repository."""
    if not repos:
        return []

    # Find the highest counts to scale each repo's score against
    max_stars = max(r["stargazers_count"] for r in repos) or 1
    max_forks = max(r["forks_count"] for r in repos) or 1
    now = datetime.now(timezone.utc)

    scored = []
    for repo in repos:
        # Stars: 0-30 points scaled to the org's most-starred repo
        star_score = (repo["stargazers_count"] / max_stars) * 30
        # Forks: 0-20 points scaled to the org's most-forked repo
        fork_score = (repo["forks_count"] / max_forks) * 20

What does this code do?

  • max_stars and max_forks find the highest star and fork counts across all repos. The or 1 prevents a division-by-zero error if every repo has zero stars or forks.
  • Each repo's star score is calculated as a proportion of the org's most-starred repo, multiplied by 30. A repo with half the max stars gets 15 points.
  • Fork scoring follows the same pattern but with a 20-point ceiling, reflecting that forks matter but less than stars for gauging community interest.
  • Continue building the calculate_health function by adding the remaining scoring logic, the results dictionary, and the sort. Copy the following code directly below the fork_score line, still inside the for loop:
        # Recency: 0-30 points, full marks if pushed within 30 days
        pushed = repo.get("pushed_at")
        if pushed:
            pushed_dt = datetime.fromisoformat(pushed.replace("Z", "+00:00"))
            days_since = (now - pushed_dt).days
            recency_score = max(0, 30 - days_since)
        else:
            recency_score = 0
        # Issues: 0-20 points, fewer issues relative to stars is better
        stars = repo["stargazers_count"]
        issues = repo["open_issues_count"]
        if stars > 0:
            issue_ratio = issues / stars
            issue_score = max(0, 20 - (issue_ratio * 100))
        else:
            issue_score = 10
        total = round(star_score + fork_score + recency_score + issue_score)
        total = min(total, 100)
        scored.append({
            "name": repo["name"],
            "score": total,
            "stars": repo["stargazers_count"],
            "forks": repo["forks_count"],
            "issues": repo["open_issues_count"],
            "pushed_at": pushed or "N/A",
            "description": repo.get("description") or "No description",
        })
    # Sort from healthiest to least healthy
    scored.sort(key=lambda r: r["score"], reverse=True)
    return scored

What does this code do?

  • Recency scoring parses the pushed_at timestamp and counts how many days ago the repo was last updated. A repo pushed today gets the full 30 points. Each day costs one point, and after 30 days the score hits zero.
  • The .replace("Z", "+00:00") swap handles GitHub's UTC timestamps. Python 3.10 cannot parse a trailing Z directly, so replacing it with +00:00 ensures compatibility across Python versions.
  • Issue scoring divides open issues by stars. A repo with 1,000 stars and 10 issues is healthier than one with 50 stars and 10 issues. Repos with zero stars get a neutral 10 points.
  • After scoring, each repo's data is packed into a dictionary and appended to the scored list. The list is then sorted by score in descending order so the healthiest repos appear first.

Why relative scoring instead of absolute thresholds?

Using absolute thresholds (e.g. "100 stars = healthy") would break for small organizations. A 50-star repo in a 5-person startup might be their best project, but it would score poorly against an absolute threshold designed for Netflix. Relative scoring means the tool produces meaningful results for any organization, regardless of size.

Score and sort the repositories

The function is ready. Now you need to call it and feed the API data through it.

  • In analyzer.py, scroll down to the bottom of your file and find these lines:
org = "netflix"
repos = fetch_repos(org)
for repo in repos:
    print(repo["name"], repo["stargazers_count"])
  • Replace those lines with the following to pass the repos through your new scoring function:
org = "netflix"
repos = fetch_repos(org)
scored_repos = calculate_health(repos)
for repo in scored_repos:
    print(repo["name"], repo["score"])
  • Save analyzer.py.

Before you run this, think: will every repo get the same score, or will the scores vary? What would cause them to differ?

  • Run the script to see health scores by running this command:
python3 analyzer.py

You should see each repository printed with a number between 0 and 100 next to its name. The repos are sorted from highest score to lowest. Scores vary because each repo has different star counts, fork counts, last-push dates, and issue counts.

Seeing a SyntaxError or IndentationError?

  • Check that the recency and issue scoring blocks are indented with 8 spaces (two levels deep inside the for loop). The scored.sort and return scored lines should be indented with 4 spaces (inside the function but outside the loop).
  • Make sure the scored.append({ block has a closing }) on its own line, indented to match scored.append.
  • Confirm from datetime import datetime, timezone appears near the top of the file, below import requests.

Ask for help: My calculate_health function is throwing an error when I run analyzer.py. Here is the traceback I see.

✔️ Awesome, I've got everything!

Great. Double check you have saved analyzer.py before moving on.

ⓧ I'd like to double check the full code

Your analyzer.py file is over 30 lines at this point. Scroll through your file and confirm these sections appear in order:

  • Three import lines at the top: import sys, import requests, and from datetime import datetime, timezone.
  • The fetch_repos(org) function from the previous step, ending with return repos.
  • The new calculate_health(repos) function, starting with the empty-list guard, containing the for repo in repos loop with four scoring blocks, the scored.append dictionary, and ending with scored.sort and return scored.
  • Four lines at the bottom: org = "netflix", repos = fetch_repos(org), scored_repos = calculate_health(repos), and the for loop printing repo["name"] and repo["score"].

Your repos are now scored and ranked. Next up, you will turn these numbers into a professional terminal report that your customer can actually read.

Generate a Professional Terminal Report

In the previous step, you built a scoring algorithm that rates each repository from 0 to 100. Running the script shows a list of names and numbers. That is useful data, but it is not something you would hand to a customer.

This step transforms the raw output into a professional terminal report. You will add aligned columns, status labels like "Healthy" and "Critical," and a summary footer that makes the data instantly scannable. You will also make the tool accept any organization as a command-line argument instead of hardcoding one.

In this step, get ready to:
  • Build a report formatter with aligned columns and status labels.
  • Accept the organization name as a command-line argument.
  • Add a summary line with repo counts and average score.
Build the report formatter

The report header will show how many API requests the user has left. GitHub sends this information in every response through rate limit headers, but fetch_repos currently throws it away.

Let's capture that data first, then build the formatter.

  • In analyzer.py, find the last two lines of the fetch_repos function:
    repos = response.json()
    return repos
  • Replace those two lines with the following code to extract rate limit headers and return them alongside the repos:
    # Extract rate limit info from response headers
    rate_remaining = response.headers.get("x-ratelimit-remaining", "?")
    rate_limit = response.headers.get("x-ratelimit-limit", "?")

    repos = response.json()
    return repos, rate_remaining, rate_limit

What changed here?

response.headers is a dictionary of metadata that GitHub sends alongside the JSON data. The x-ratelimit-remaining header tells you how many requests you have left out of x-ratelimit-limit total.

By returning a tuple (repos, rate_remaining, rate_limit), the caller can unpack all three values at once.

  • Now scroll to the bottom of the file and find this line:
repos = fetch_repos(org)
  • Update it to unpack the three return values:
repos, rate_remaining, rate_limit = fetch_repos(org)
  • Save analyzer.py and run the script to confirm nothing breaks:
python3 analyzer.py

You should see the same list of repository names and scores as before, with no errors. The rate limit data is now captured and ready for the report header.

Seeing a ValueError about unpacking?

If you see ValueError: not enough values to unpack, make sure you updated both the return line in fetch_repos and the assignment line at the bottom of the file. Both need to use the three-value tuple.

help me fix a ValueError when unpacking the return value of fetch_repos

Now add two new functions that format the scored data into a professional report.

  • Copy this code below the calculate_health function in analyzer.py:
def get_status(score):
    """Return a status label based on health score."""
    if score >= 70:
        return "Healthy"
    elif score >= 40:
        return "Warning"
    else:
        return "Critical"


def print_report(org, scored_repos, rate_remaining, rate_limit):
    """Print a formatted health report to the terminal."""
    now = datetime.now().strftime("%Y-%m-%d %H:%M")

    # Print report header with org info and rate limit
    print()
    print("=" * 72)
    print(f"  GitHub Repository Health Report")
    print(f"  Organization: {org}")
    print(f"  Generated: {now}")
    print(f"  API requests remaining: {rate_remaining}/{rate_limit}")
    print("=" * 72)
    print()
    # Print each repo in aligned columns with status label
    print(f"  {'#':<4} {'Repository':<32} {'Score':<8} {'Status':<10}")
    print(f"  {'-'*4} {'-'*32} {'-'*8} {'-'*10}")

    for i, repo in enumerate(scored_repos, 1):
        name = repo["name"][:30]
        status = get_status(repo["score"])
        print(f"  {i:<4} {name:<32} {repo['score']:<8} {status:<10}")

What does this code do?

  • get_status maps each health score to a human-readable label: "Healthy" for 70+, "Warning" for 40-69, "Critical" for below 40.
  • print_report prints a header with the org name, timestamp, and remaining API requests. It then loops through each repo and prints its rank, name, score, and status in aligned columns.
  • The format specifiers like :<32 left-align text within a fixed-width column. This keeps the output neat and scannable regardless of how long a repo name is.
  • Now find the for loop at the bottom of the file:
for repo in scored_repos:
    print(repo["name"], repo["score"])
  • Replace those two lines with this single line to use your new formatter:
print_report(org, scored_repos, rate_remaining, rate_limit)
  • Save analyzer.py and run the script:
python3 analyzer.py

You should see a formatted report with a header, aligned columns, and a status label next to each repository. The output is now structured and scannable.

Report not showing up?

Make sure get_status and print_report are placed after calculate_health and before the bottom-level code. Python reads the file top to bottom, so calling a function before it is defined causes a NameError.

help me debug why my print_report function is not found or the report is not displaying

Accept the organization name as a command-line argument

Right now the organization name is hardcoded as "netflix" at the bottom of the file. A real CLI tool should accept it as an argument so the user can analyze any organization.

  • Delete all the loose code at the bottom of analyzer.py (the lines starting with org = "netflix") and replace it with this main function and entry point:
def main():
    # Check that the user provided an organization name
    if len(sys.argv) < 2:
        print("Usage: python3 analyzer.py <org-name>")
        print("Example: python3 analyzer.py netflix")
        sys.exit(1)

    org = sys.argv[1]

    # Fetch, score, and display the report
    print(f"Fetching repositories for '{org}'...")
    repos, rate_remaining, rate_limit = fetch_repos(org)

    if not repos:
        print(f"No public repositories found for '{org}'.")
        sys.exit(0)

    scored_repos = calculate_health(repos)
    print_report(org, scored_repos, rate_remaining, rate_limit)


if __name__ == "__main__":
    main()

What does this code do?

  • sys.argv is a list of command-line arguments. sys.argv[0] is the script name (analyzer.py), and sys.argv[1] is the first argument the user provides.
  • If no argument is given, the script prints a usage message and exits instead of crashing.
  • The if __name__ == "__main__": guard ensures main() only runs when you execute the script directly, not when importing it as a module.
  • Save analyzer.py and run the tool with an organization name:
python3 analyzer.py netflix

You should see the same formatted report as before, with Netflix's repositories listed by health score.

  • Now test what happens with no arguments by running:
python3 analyzer.py

You should see the usage message: Usage: python3 analyzer.py <org-name>.

Script running with old hardcoded org?

Make sure you deleted the old lines org = "netflix", repos, rate_remaining, rate_limit = fetch_repos(org), scored_repos = calculate_health(repos), and print_report(...). All of that code now lives inside main().

help me fix my analyzer.py so it reads the organization from a command-line argument

Add a summary to the report

The report shows individual repos, but a customer also wants the big picture. A summary line at the bottom gives them an instant read on the overall health of the organization.

  • In analyzer.py, find the for loop inside print_report that prints each repo row. Add this code directly after that loop (still inside the print_report function):
    # Print summary statistics
    print()
    print("-" * 72)

    total = len(scored_repos)
    avg = round(sum(r["score"] for r in scored_repos) / total) if total else 0
    healthy = sum(1 for r in scored_repos if r["score"] >= 70)
    warning = sum(1 for r in scored_repos if 40 <= r["score"] < 70)
    critical = sum(1 for r in scored_repos if r["score"] < 40)

    print(f"  Total: {total} repos | Avg Score: {avg}")
    print(f"  Healthy: {healthy} | Warning: {warning} | Critical: {critical}")
    print("=" * 72)
    print()

What does this code do?

  • The summary counts how many repos fall into each status category using generator expressions like sum(1 for r in scored_repos if r["score"] >= 70).
  • It also calculates the average score across all repos, giving the customer a single number to judge the org's overall health.

Before you run this, what do you think the bottom of the report will look like? Think about what a summary of Netflix's repos would show.

  • Save analyzer.py and run the tool:
python3 analyzer.py netflix

You should see the full report: a header with the org name and rate limit, aligned columns with status labels, and a summary footer showing total repos, average score, and the Healthy/Warning/Critical breakdown.

Summary not appearing?

Check that the summary code is indented at the same level as the for loop inside print_report. If it is less indented, Python thinks it is outside the function. If it is more indented, Python treats it as part of the for loop body and will print a summary after every single repo.

help me fix the summary section of my print_report function

✔️ Awesome, I've got everything!

Great. Double check you have saved analyzer.py before moving on.

ⓧ I'd like to double check the full code

Your analyzer.py should contain these sections in order. Scroll through your file and confirm each one is present:

  • Imports: sys, requests, datetime and timezone.
  • fetch_repos(org) function with error handling for 404/403, rate limit header extraction, and a return of repos, rate_remaining, rate_limit.
  • calculate_health(repos) function unchanged from the previous step.
  • get_status(score) function returning "Healthy", "Warning", or "Critical".
  • print_report(org, scored_repos, rate_remaining, rate_limit) function with the header, aligned columns, and summary statistics.
  • main() function with sys.argv handling and the if __name__ == "__main__": guard at the very bottom.

Your report looks great when everything goes right. But what happens when a user misspells the organization name, or runs the tool too many times and hits the rate limit? In the next step, you will make the tool handle those failures gracefully.

Handle API Errors Like a Real FDE

Your tool works when everything goes right. Point it at Netflix and you get a polished health report with scores, status labels, and a summary.

But what happens when the network drops or GitHub takes too long to respond? Right now, requests.get() would throw a raw Python exception and crash the tool with an ugly traceback. A real Forward Deployment Engineer ships tools that handle failures gracefully, because customer environments are never perfect.

In this step, get ready to:
  • Add try/except handling for network connection failures and timeouts.
  • Verify that every error path produces a friendly message instead of a crash.
Spot the gap in your error handling

Your fetch_repos function already handles HTTP-level errors: a 404 if the org name is wrong, a 403 if you hit the rate limit, and a catch-all for any other non-200 status code. That is solid work.

  • Open analyzer.py in VS Code and scroll to the fetch_repos function.
  • Find this line:
    response = requests.get(url, headers=headers, params=params, timeout=10)

This line is completely unprotected. If your internet drops, requests raises a ConnectionError. If GitHub takes longer than 10 seconds, it raises a Timeout. Both would crash the tool with a traceback the customer should never see.

Wrap the API call in a try/except block
  • In analyzer.py, replace the bare requests.get line you just found with this try/except block:
    try:
        response = requests.get(url, headers=headers, params=params, timeout=10)
    except requests.exceptions.ConnectionError:
        print("Could not connect to GitHub. Check your internet connection.")
        sys.exit(1)
    except requests.exceptions.Timeout:
        print("Request timed out. Try again later.")
        sys.exit(1)

What does this code do?

  • try: wraps the requests.get call so Python catches any exception instead of crashing.
  • requests.exceptions.ConnectionError fires when the network is unreachable. This covers dropped WiFi, DNS failures, and refused connections.
  • requests.exceptions.Timeout fires when GitHub does not respond within the 10-second limit you set.
  • Both handlers print a clear message and call sys.exit(1) so the tool exits cleanly instead of dumping a traceback.

Why catch these separately from HTTP errors?

HTTP status codes (404, 403) mean GitHub received your request and replied with an error. Network exceptions mean the request never reached GitHub at all. They happen at different layers, so they need different handling.

  • Save analyzer.py.
  • Confirm the tool still works normally by running:
python3 analyzer.py netflix

You should see the full health report for Netflix, exactly as before. The try/except only activates when something goes wrong.

Seeing a SyntaxError or IndentationError?

  • Make sure the try: line is indented to the same level as the old response = requests.get(...) line (one level inside the function).
  • The response = requests.get(...) line must be indented one level deeper than try: (two levels inside the function).
  • Check that the except lines are at the same indent level as try:.
  • Make sure the existing if response.status_code == 404: block is still below the try/except, not inside it.

Help me fix my try/except indentation in analyzer.py

✔️ Awesome, I've got everything!

Great. Double check you have saved analyzer.py.

ⓧ I'd like to double check the full code

The full file is long, so scroll through analyzer.py and confirm these sections are present in order:

  • Imports: sys, requests, datetime and timezone at the top.
  • fetch_repos(org): requests.get wrapped in a try/except catching ConnectionError and Timeout, followed by status code checks for 404, 403, and non-200.
  • calculate_health(repos): scoring logic with star, fork, recency, and issue components.
  • get_status(score): returns Healthy, Warning, or Critical.
  • print_report(...): formatted report with header, aligned columns, API requests remaining, and summary.
  • main(): reads org from sys.argv[1], prints usage with example if no argument provided.
Verify every error path

Your tool now handles three layers of failure: network exceptions, HTTP status codes, and missing arguments. Time to prove each one works.

Before you run the next command, predict what you will see. Will the tool crash or print a friendly message?

  • Test the 404 error path by running:
python3 analyzer.py nonexistent-org-12345

You should see a clean one-line message: Organization 'nonexistent-org-12345' not found. Check the spelling and try again. No traceback, no crash.

  • Test the missing-argument path by running:
python3 analyzer.py

You should see the usage instructions with an example: Usage: python3 analyzer.py <org-name>.

  • Confirm the normal path still works by running:
python3 analyzer.py netflix

You should see the full health report with the header, aligned columns, status labels, and the API requests remaining line showing how many of your 60 hourly requests are left.

Why does the rate limit line matter?

The GitHub REST API allows 60 unauthenticated requests per hour. Your report reads the x-ratelimit-remaining and x-ratelimit-limit headers from every response and displays them in the header.

This gives the user a clear warning before they run out. If they hit the limit, the 403 handler you already have catches it with a helpful message.

Getting a traceback instead of a friendly message?

  • If you see NameError, check that your except lines reference requests.exceptions.ConnectionError (not just ConnectionError).
  • Make sure the if response.status_code == 404: block starts after the try/except, not inside it. It should be at the same indentation level as try:.

Help me debug my error handling in fetch_repos

Your tool is complete. It fetches live data, scores repository health, prints a professional report, and handles every common failure mode without crashing. That is exactly the kind of tool a Forward Deployment Engineer ships to a customer on day one.

Secret mission

Add a Language Breakdown Deep Dive

Your customer loves the health report, but now they want to know what languages each project uses. Add a --deep flag that fetches the language breakdown for each repository and displays the top 3 languages as percentages in the report.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files or delete them entirely.

Resources you used:

  • The repo-health/ project folder on your Desktop, containing analyzer.py.
  • The requests library installed in your Python environment.

Keep everything running

No action needed. Choose this if you plan to keep using or extending your repo-health tool.

  • Your project stays in the repo-health/ folder on your Desktop, ready to run anytime.
  • The requests library remains installed and available for other Python projects too.

Pause - I'll come back to this later

There are no running processes or cloud resources to pause. Your project is a local Python script with no ongoing background activity.

  • Close VS Code if you want to free up memory.
  • Your project folder and installed packages will be right where you left them when you come back.

Delete - I don't want to use this again

Remove all project files from your machine and optionally uninstall the packages you installed.

  • Delete the repo-health folder from your Desktop by running the command for your operating system below.

macOS

rm -rf ~/Desktop/repo-health

Windows

Remove-Item -Recurse -Force ~\Desktop\repo-health

Linux

rm -rf ~/Desktop/repo-health
  • Optionally, uninstall the requests library by running:
pip uninstall requests

You will be asked to confirm the uninstall. Type y and press Enter to proceed.

Nice Work!

Nice Work!

Nice work! You just built a Python command-line tool that pulls live data from the GitHub REST API and turns it into an actionable health report a customer can scan in seconds.

You've learned how to:

  • Connected to the GitHub REST API, parsed live JSON responses, and transformed raw repository metrics into a weighted health score (0-100) based on stars, forks, recency, and open issues.
  • Built graceful HTTP error handling for 404s, 403s, and network failures so the tool never crashes on bad input or rate limits.
  • Formatted the results into a professional terminal report with aligned columns, status labels (Healthy, Warning, Critical), and summary statistics that a customer can act on immediately.
  • Completed an optional Secret Mission to push your skills further.

Ready to quiz yourself?