Introduction: Keeping Your Recipe Data Clean

Welcome back! So far, you have learned how to build and use API endpoints to retrieve, search, and rate recipes in your cooking helper app. As your app grows and more recipes are added, it is important to make sure your data stays clean and reliable. One common problem in real-world applications is duplicate data — when the same recipe appears more than once in your database.

Duplicate recipes can confuse users, make search results messy, and even affect features like ratings and recommendations. In this lesson, you will learn how to create a script to identify and remove duplicate recipes from your application. This is an important step in keeping your app’s data accurate and professional.

By the end of this lesson, you’ll know how to find and safely delete duplicate recipes, making your cooking helper app more reliable for users.

How Duplicate Recipes Happen

Before we look at the solution, let’s talk about how duplicate recipes can appear in your database. Sometimes, users might add the same recipe twice by mistake. Other times, small differences — like extra spaces or different capitalization — can make two recipes look different to a computer, even though they are the same to a person.

For example, these two names would be considered duplicates by a human, but not always by a computer:

  • "Chocolate Cake"
  • " chocolate cake "

In our project, we consider recipes to be duplicates if their names are the same when you ignore spaces at the beginning or end and treat uppercase and lowercase letters as the same. This is called normalizing the name.

Understanding the Duplicate Recipe Removal Process

Let’s break down how the duplicate recipe removal process works, step by step.

1. Connecting to Your App and Database

To interact with your application’s data outside of a standard web request (like in a standalone script), you need to set up the environment manually. This involves telling the system where your project files are located and which configuration settings to use.

import os
import sys
import django

# 1. Find the project root directory
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.abspath(os.path.join(CURRENT_DIR, ".."))

# 2. Add the project root to the system path so we can import our models
if PROJECT_ROOT not in sys.path:
    sys.path.insert(0, PROJECT_ROOT)

# 3. Specify which settings file to use
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cooking_helper.settings")

# 4. Initialize the application framework
django.setup()

from recipes.models import Recipe
  • We adjust sys.path so the script can "see" the recipes module.
  • django.setup() initializes the framework, allowing us to use models and database tools.
2. Normalizing Recipe Names

To find duplicates, the script needs to compare recipe names in a way that ignores spaces and capitalization. This is done with a helper function:

def normalize_name(name: str) -> str:
    return name.strip().lower()
  • strip() removes spaces at the beginning and end.
  • lower() makes all letters lowercase.

For example, " Chocolate Cake " becomes "chocolate cake".

3. Finding and Listing Duplicates

The script loads all recipes from the database and groups them by their normalized names. To make the script efficient, we use prefetch_related to load the ingredients at the same time we load the recipes.

from collections import defaultdict

def find_duplicate_recipes(confirm: bool = True) -> None:
    # Fetch recipes and pre-load related ingredients for efficiency
    recipes = Recipe.objects.all().prefetch_related("ingredients")

    name_map = defaultdict(list)
    for recipe in recipes:
        key = normalize_name(recipe.name or "")
        name_map[key].append(recipe)

    # Filter only those groups that have more than one recipe
    duplicates = {name: recs for name, recs in name_map.items() if len(recs) > 1}

    if not duplicates:
        print("No duplicate recipes found.")
        return

    print("Duplicate recipes found:\n")
    for name, recs in duplicates.items():
        print(f"Recipe Name: {name}")
        for recipe in recs:
            # Use count() on the ingredients relationship
            print(f"  - ID: {recipe.id}, Ingredients: {recipe.ingredients.count()}")
        print()
  • prefetch_related("ingredients") ensures that when we check the ingredient count, the script doesn't have to go back to the database for every single recipe individually.
  • recipe.ingredients.count() is an efficient way to see how many items are linked to that recipe.
4. Safely Removing Duplicates

Once duplicates are identified, the script asks for confirmation. If confirmed, it uses an atomic transaction to ensure the deletion happens safely. A transaction ensures that either all the deletions are successful, or none of them are, preventing the data from getting stuck in a half-deleted state if an error occurs.

from django.db import transaction

    if confirm:
        choice = input("Do you want to delete duplicates and keep one entry for each? (y/N): ").strip().lower()
        if choice != "y":
            print("Aborted. No changes made.")
            return

    # Use an atomic block for safe database operations
    with transaction.atomic():
        for recs in duplicates.values():
            to_keep = recs[0]    # Keep the first one found
            to_delete = recs[1:] # Select the rest for deletion
            for recipe in to_delete:
                recipe.delete() # Remove the duplicate from the database

    print("Duplicates removed.")
  • with transaction.atomic() creates a protective bubble around the deletion process.
  • recipe.delete() directly removes the record from the database. Because of the way our database is structured, any related data (like reviews) linked to these specific duplicate IDs will also be cleaned up automatically.
5. Running the Duplicate Removal

To run the script, we call the function within a standard entry point block:

if __name__ == "__main__":
    find_duplicate_recipes(confirm=True)
Example: Running the Script

Let’s see what happens when you run the duplicate removal process.

If there are no duplicates, you’ll see:

No duplicate recipes found.

If duplicates are found, you’ll see something like:

Duplicate recipes found:

Recipe Name: chocolate cake
  - ID: 1, Ingredients: 5
  - ID: 12, Ingredients: 5

Do you want to delete duplicates and keep one entry for each? (y/N):

If you type y and press Enter, the script will remove the extra copies and show:

Duplicates removed.

If you press Enter or type anything else, it will show:

Aborted. No changes made.
Summary and What’s Next

In this lesson, you learned how to maintain high-quality data by identifying and removing duplicate recipes. You saw how to set up a standalone script environment, use name normalization to find hidden duplicates, and perform safe deletions using atomic transactions.

Good data hygiene is vital for any real-world app. By keeping your records unique, you make your app faster, more reliable, and much easier for users to navigate.

Congratulations on reaching the end of this course! You now have the skills to build, search, and maintain a high-quality recipe API. Great job!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal