Categories Subcategories In Python (Simple Example)

Welcome to a tutorial on how to deal with categories and subcategories in Python. So you have to work with categories and subcategories in your project? Well, the bad news is that it involves recursion. Let us walk through a simple example – Read on!

 

TABLE OF CONTENTS

 

 

DOWNLOAD & NOTES

Here is the download link to the example code, so you don’t have to copy-paste everything.

 

EXAMPLE CODE DOWNLOAD

Source code on GitHub Gist

Just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.

 

SORRY FOR THE ADS...

But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.

Buy Me A Coffee Code Boxx eBooks

 

 

PYTHON CATEGORIES

All right, let us now get into the example of working with categories and subcategories in Python.

 

QUICK SETUP

  • Create a virtual environment virtualenv venv.
  • Activate it – venv\Scripts\activate (Windows) venv/bin/activate (Linux/Mac)

 

PART 1) THE DATABASE

1A) CATEGORY TABLE

S1A_category.sql
CREATE TABLE category (
  category_id INTEGER,
  parent_id INTEGER DEFAULT 0,
  category_name TEXT NOT NULL,
  PRIMARY KEY("category_id" AUTOINCREMENT)
);
CREATE INDEX parent_id ON category (parent_id);

First, let us begin with the database. To keep things simple, we will be using the newbie-friendly SQLite. In your own project, you can use MYSQL, Oracle, Postgre, Mongol, and whatever else. But yep, a simple parent_id field is all we need.

  • Categories with parent_id=0 are “main categories”.
  • Categories with parent_id>0 are “subcategories”.

 

 

1B) DUMMY CATEGORIES

S1A_category.sql
INSERT INTO `category` (`category_id`, `parent_id`, `category_name`) VALUES
(1, 0, 'Electronics'),
(2, 1, 'Computers'),
(3, 1, 'Cameras'),
(4, 2, 'Desktop'),
(5, 2, 'Laptop');

A quick example of how the parent_id work:

 

1C) CREATE THE DATABASE

S1B_create.sql
# (A) LOAD PACKAGES
import sqlite3, os
from sqlite3 import Error

# (B) DATABASE + SQL FILE
DBFILE = "category.db"
SQLFILE = "S1A_category.sql"

# (C) DELETE OLD DATABASE IF EXIST
if os.path.exists(DBFILE):
  os.remove(DBFILE)

# (D) IMPORT SQL
conn = sqlite3.connect(DBFILE)
  with open(SQLFILE) as f:
conn.executescript(f.read())
conn.commit()
conn.close()
print("Database created!")

Just run this to create the actual category.db database file.

 

 

PART 2) RECURSIVE GET CATEGORY

S2_lib.py
# (A) LOAD SQLITE MODULE
import sqlite3
DBFILE = "category.db"
 
# (B) GET CATEGORIES
  def get (parent=0):
  # (B1) CONNECT
  conn = sqlite3.connect(DBFILE)
  cursor = conn.cursor()
 
  # (B2) GET CATEGORY WITH PARENT ID
  cursor.execute(
    "SELECT * FROM `category` WHERE `parent_id`=?",
    (parent,)
  )
  rows = cursor.fetchall()
 
  # (B3) NO CHILDREN IN THIS CATEGORY
  if len(rows)==0:
    return None

  # (B4) ARRANGE DATA & RECURSIVE
  data = {}
  for r in rows:
    data[r[0]] = {
      "n" : r[2],
      "c" : get(r[0])
    }
    return data

Now comes the ugly part of getting the categories. For those who are lost, here’s a quick trace of working with this example:

  • When we call get(), this will run SELECT * FROM `category` WHERE `parent_id`=0.
  • The data will be arranged into data[1] = { n : ELECTRONICS, c : get(1) }.
  • In the second round, get() will run SELECT * FROM `category` WHERE `parent_id`=1.
  • The data will be arranged into data[CATEGORY-ID] = { n : CATEGORY-NAME, c : RECURSION }.

Yes, get() is a recursive function that will “drill down” to get all the child categories.

 

 

PART 3) DRAW CATEGORIES

S3_draw.py
# (A) LOAD MODULES
import S2_lib as cat
 
# (B) DRAW CATEGORIES
def draw (data, level=0):
  for id, cat in data.items():
    # (B1) INDENTATION
    if level>0:
      for i in range(level):
        print(" ", end="")
 
    # (B2) CATEGORY ID & NAME
    print(f"({id}) {cat['n']}")
    if cat["c"] is not None:
      draw(cat["c"], level+1)
 
# (C) GO!
draw(cat.get())

Now that we have all the categories, the last step is to draw them out nicely. You may think that we are done with recursion, but no… We need yet another recursive method to draw the categories.

(1) Electronics
  (2) Computers
   (4) Desktop
   (5) Laptop
  (3) Cameras

P.S. I am just being lazy here. Feel free to draw your own “nice list” in HTML, or whatever GUI interface – I will leave some links below to my simple dropdown and tree menu tutorials.

 

PART 4) ADD EDIT DELETE CATEGORY

Lastly, this part is kind of optional. But if you have to deal with the “category admin”, here are a couple more examples to help you.

 

4A) ADD & EDIT CATEGORY

S2_lib.py
# (C) SAVE CATEGORY
def save (name, parent, id=None):
  # (C1) CONNECT TO DATABASE
  conn = sqlite3.connect(DBFILE)
  cursor = conn.cursor()

  # (C2) ADD OR UPDATE SQL
  if id is None:
    sql = "INSERT INTO `category` (`category_name`, `parent_id`) VALUES (?,?)"
    data = (name, parent,)
  else:
    sql = "UPDATE `category` SET `category_name`=?, `parent_id`=? WHERE `category_id`=?"
    data = (name, parent, id,)

  # (C3) GO!
  cursor.execute(sql, data)
  conn.commit()
  conn.close()
  return True

Haiyaa. So easy, just do the usual SQL insert and update.

 

 

4B) THE “CHANGE PARENT CATEGORY” PAIN

S2_lib.py
# (D) GET CHILD CATEGORIES
def getchildren (id):
  conn = sqlite3.connect(DBFILE)
  cursor = conn.cursor()
  cursor.execute(
    "SELECT `category_id` FROM `category` WHERE `parent_id`=?",
    (id,)
  )
  cat = ()
  for r in cursor.fetchall():
    cat = cat + r
  for id in cat:
    cat = cat + getchildren(id)
  return cat

# (E) GET "LEGAL" PARENT IDS
def getswitchable (id):
  # (E1) PARENT ID CANNOT BE SELF AND CHILDREN
  illegal = getchildren(id)
  illegal = illegal + (id,)
  illegal = ",".join([str(v) for v in illegal])

  # (E2) GET ALL "LEGAL" PARENT ID
  conn = sqlite3.connect(DBFILE)
  cursor = conn.cursor()
  cursor.execute(f"SELECT `category_id` FROM `category` WHERE `category_id` NOT IN ({illegal})")
  cat = (0,)
  for r in cursor.fetchall():
    cat = cat + r
  return cat

Now, the SQL insert and update are easy. But consider these:

  • If we change “computers” to a “root category” – Ok, no problem.
  • If we change “computers” to a child of “camera” – Strange, but the system can still work.
  • When we change “computers” to a child of “laptop” – Both laptop and desktop become orphans.

So yes, there must be checks and rules when changing the parent category. Very simply – A category can only “move up” or “move beside”. Never into a child category within the same branch.

 

4C) DELETE CATEGORY

S2_lib.py
# (F) DELETE CATEGORY
def delete (id):
  # (F1) CONNECT TO DATABASE
  conn = sqlite3.connect(DBFILE)
  cursor = conn.cursor()

  # (F2) REVERT ALL CHILDREN TO ROOT
  children = getchildren(id)
  if (len(children)>0):
    children = ",".join([str(v) for v in children])
    cursor.execute(f"UPDATE `category` SET `parent_id`=0 WHERE `category_id` IN ({children})")

  # (F3) DELETE CATEGORY
  cursor.execute(
    "DELETE FROM `category` WHERE `category_id`=?",
    (id,)
  )
  conn.commit()
  conn.close()
  return True

Deleting the category should be straightforward, but remember to deal with the child categories as well. In this example, we revert all the children to “root”. If you want, you can do a cascade delete.

 

EXTRAS

That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.

 

SINGLE PARENT ONLY

As you can see, parent_id is easy to implement, but the limitation is – A category can only have one parent. If you want to allow multiple parents, another way is to create a category_to_category table with 2 fields – category_id and parent_id. This way, one category can have multiple parents. But be warned, the complexity will also go up a whole load.

 

LINKS & REFERENCES

 

THE END

Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!