How to Build a Tiered List Maker with Python

Have you ever wanted an easy way to categorize and rank a collection of items? Whether you‘re evaluating albums, movies, restaurants or something else entirely, a tiered list provides a great visual format for slotting items into ranked buckets. In this article, we‘ll walk through how to build a fully-functional tiered list maker using Python.

We‘ll cover everything from the basic data structures to the visual rendering of the final list. By the end, you‘ll have a flexible command-line tool that can generate tiered lists for any domain. Let‘s get started!

What is a Tiered List?

A tiered list, also known as a tier list or tier ranking, is a way of categorizing items into ranked groups called tiers. Each tier represents a certain level of quality or preference. Typically, the tiers are labelled with letters like S, A, B, C, D and items are placed into the tiers based on how well they meet certain criteria.

Here‘s a simple example of a tiered list ranking some common fruits:

S Tier: Mango, Watermelon
A Tier: Strawberry, Pineapple
B Tier: Banana, Orange
C Tier: Grapefruit, Honeydew
D Tier: Cantaloupe, Papaya

Tiered lists originated in gaming communities as a way to rank characters based on their combat strength. They‘ve since expanded into a general format for comparing items of any type. Their visual nature makes it easy to see both the overall ranking and where each item stacks up.

Setting Up the Environment

To build our tiered list maker, we‘ll be using Python 3. We‘ll also leverage a few popular libraries:

  • Pillow for generating images of our tiered lists
  • Pandas for handling our tiered list data in a structured way
  • PyInputPlus for getting validated user input

You can install these dependencies using pip:

pip3 install Pillow pandas pyinputplus

We‘ll be developing this as a command-line tool, so no need for any web frameworks or UI libraries. Any code editor will work, though one with good Python support like VS Code or PyCharm is recommended.

Designing the Data Model

Let‘s think about how we want to represent a tiered list in code. At its core, a tiered list is:

  1. A set of labels for the tiers (S, A, B, C, D by default)
  2. A mapping of items to tiers
  3. Metadata like the list title, creation date, etc.

We could define a TieredList class to encapsulate this:

from dataclasses import dataclass
from typing import List, Dict

TIERS = [‘S‘, ‘A‘, ‘B‘, ‘C‘, ‘D‘] 

@dataclass
class TieredList:
    title: str
    tiers: List[str] = TIERS
    items: Dict[str, str] = field(default_factory=dict)  

Here we‘re using the dataclasses module to define a class with typed fields. The tiers field defaults to the standard S/A/B/C/D labels. The items field is a dict that will map item names to their assigned tier.

Dataclasses are a concise way to represent structured data in Python. By using typing, we get better editor support and self-documenting code.

The List Manager

Next let‘s create a ListManager class that will handle the core operations:

from typing import Dict, List
import pandas as pd

class ListManager:
    def __init__(self):
        self.lists: Dict[str, TieredList] = {}

    def create(self, title: str):
        if title in self.lists:
            raise ValueError(f"List ‘{title}‘ already exists")

        self.lists[title] = TieredList(title)
        print(f"Created new list: {title}")

    def add_item(self, list_name: str, item: str, tier: str):
        tiered_list = self.lists.get(list_name)

        if not tiered_list:
            raise ValueError(f"No list named {list_name}")
        if tier not in tiered_list.tiers:
            raise ValueError(f"Invalid tier: {tier}")

        tiered_list.items[item] = tier
        print(f"Added ‘{item}‘ to {tier} tier in {list_name}")

    def view(self, list_name: str) -> pd.DataFrame:
        tiered_list = self.lists.get(list_name)

        if not tiered_list:
            raise ValueError(f"No list named {list_name}")

        data: Dict[str, List[str]] = {tier: [] for tier in TIERS}
        for item, tier in tiered_list.items.items():
            data[tier].append(item)

        return pd.DataFrame(data)

The ListManager maintains a dictionary of TieredList objects. It exposes methods to:

  • create a new empty list with a given title
  • add an item to a specific tier in a list
  • view a list as a Pandas DataFrame with items grouped by tier

The view method converts the TieredList into a DataFrame. This allows for easy display of the list data in a tabular format.

Using a class here gives us flexibility to add more operations later. It also encapsulates the tiered list data and manipulation logic.

Getting User Input

Now let‘s set up a simple command-line interface using PyInputPlus:

import pyinputplus as pyip

def prompt_new_list():
    title = pyip.inputStr("Enter a title for the new list: ")
    return title

def prompt_add_item(tiers: List[str]):
    item_name = pyip.inputStr("Enter the item to add: ")
    tier = pyip.inputMenu(tiers, numbered=True)
    return item_name, tier

def prompt_action():
    return pyip.inputMenu([‘New List‘, ‘Add Item‘, ‘View List‘, ‘Quit‘], numbered=True)

PyInputPlus has several helpful functions for getting and validating user input.

  • inputStr prompts the user to enter a string.
  • inputMenu presents a list of options and has the user choose one.

These prompts will be used in the main application loop to get info from the user on what actions to take and details for those actions.

The Main App

Now we‘re ready to put it all together into the main application:

def main():
    print("Welcome to TierMaker!")
    manager = ListManager()

    while True:
        action = prompt_action()
        if action == ‘Quit‘:
            print("Goodbye!")
            break

        if action == ‘New List‘:
            title = prompt_new_list()
            manager.create(title)

        elif action == ‘Add Item‘:
            list_name = pyip.inputMenu(manager.lists.keys())
            item_name, tier = prompt_add_item(TIERS)
            manager.add_item(list_name, item_name, tier)

        elif action == ‘View List‘:
            list_name = pyip.inputMenu(manager.lists.keys())
            print(manager.view(list_name))

if __name__ == ‘__main__‘:
    main()

The main function is the entry point to our app. It:

  1. Creates a new ListManager
  2. Prompts the user to choose an action
  3. Executes the appropriate subflow for the chosen action
  4. Loops until the user chooses to quit

This provides a simple but functional interface for interacting with our tiered list maker. Users can create new lists, add items to a list, and view the contents of a list.

Generating List Images

As a final touch, let‘s add the ability to render a tiered list as an image. We can use Pillow to create the graphic and Pandas to prepare the data:

from PIL import Image, ImageDraw, ImageFont

def render_list(tiers: pd.DataFrame, title: str):
    num_tiers = len(tiers.columns)
    num_items = tiers.apply(lambda x: len(x.dropna())).max() 
    item_height = 100
    tier_width = 300
    padding = 10

    font = ImageFont.truetype(‘Arial.ttf‘, size=24)
    title_font = ImageFont.truetype(‘Arial.ttf‘, size=36)

    img_width = (tier_width + padding) * num_tiers 
    img_height = item_height * (num_items + 1)  # +1 for tier labels
    img = Image.new(‘RGB‘, (img_width, img_height), color=‘white‘)
    draw = ImageDraw.Draw(img)

    # Draw tier labels
    for i, tier in enumerate(tiers.columns):
        x = i * (tier_width + padding) 
        y = 0
        draw.rectangle((x, y, x + tier_width, y + item_height), fill=‘lightblue‘)
        draw.text((x + tier_width/2, y + item_height/2), tier, font=font, fill=‘black‘, anchor=‘mm‘)

    # Draw items
    for i, tier in enumerate(tiers.columns):
        x = i * (tier_width + padding)
        for j, item in enumerate(tiers[tier].dropna()):
            y = (j+1) * item_height
            draw.text((x + tier_width/2, y + item_height/2), item, font=font, fill=‘black‘, anchor=‘mm‘)

    # Draw title
    draw.text((img_width/2, padding), title, font=title_font, fill=‘black‘, anchor=‘ma‘)

    img.save(f"{title}.png")
    print(f"Rendered list to {title}.png")

def view_and_render(manager: ListManager):
    list_name = pyip.inputMenu(manager.lists.keys())
    tier_list = manager.lists[list_name]
    df = manager.view(list_name)
    print(df)
    render_list(df, tier_list.title)

The render_list function takes a tiered list DataFrame and a title, and generates a PNG image of the list. It uses Pillow to:

  1. Create a blank image with dynamic dimensions based on the number of tiers and items
  2. Draw labeled rectangles for each tier
  3. Write the item names into the appropriate tier box
  4. Add a title to the image
  5. Save the result as a PNG

The view_and_render function prompts the user to select a list, displays it as a DataFrame, and then calls render_list to generate an image of it.

To use this feature, we just need to add a new menu option in our main loop:

    elif action == ‘Render List‘:
        view_and_render(manager)

Serialization and Persistence

As a bonus feature, we could add support for saving and loading tiered lists using json serialization. This would allow users to persist their lists between sessions.

First we need to make our TieredList and ListManager classes serializable:

import json

class TieredList:
    ... 

    def to_dict(self):
        return {
            ‘title‘: self.title,
            ‘tiers‘: self.tiers,
            ‘items‘: self.items
        }

    @classmethod
    def from_dict(cls, data):
        return cls(**data)


class ListManager:
    ...

    def save(self, filename):
        data = {name: tl.to_dict() for name, tl in self.lists.items()}
        with open(filename, ‘w‘) as f:
            json.dump(data, f)

    def load(self, filename):
        with open(filename, ‘r‘) as f:
            data = json.load(f)
            self.lists = {name: TieredList.from_dict(tl) for name, tl in data.items()}

We add to_dict and from_dict methods to TieredList to convert between the object and a dictionary representation.

We add save and load methods to ListManager that write the lists to a JSON file and read them back, using the to_dict/from_dict methods.

Finally we add a couple new actions to the main loop:

    elif action == ‘Save Lists‘:
        filename = pyip.inputStr("Enter filename: ")
        manager.save(filename)
        print(f"Saved lists to {filename}")

    elif action == ‘Load Lists‘:
        filename = pyip.inputStr("Enter filename: ")
        manager.load(filename)  
        print(f"Loaded lists from {filename}")

And we‘re all set! Users can now save their lists to a file and load them back later.

Wrap Up

With that, we have a fully functional tiered list maker. Users can create lists, populate them with categorized items, view and render lists, and save and load data. The code is structured in a way that would be fairly easy to extend with new features.

Some potential improvements:

  • A GUI or web interface in addition to the CLI
  • Support for images in list items
  • Allowing custom tier labels
  • Built-in item suggestions based on a database or API
  • Shareable links for lists

Tiered lists offer a fun and intuitive way to categorize any collection of comparable items. With Python, Pillow, and Pandas, we were able to put together a capable tiered list maker in a couple hundred lines of code. Feel free to use this as a starting point for your own tiered list projects. Happy ranking!

Similar Posts